API

Правила дизайну: іменування та стандарти

Принципи дизайну API: явне краще за неявне, конкретні імена, стандарти дат (ISO 8601), валют, дробових чисел, консистентність інтерфейсів та підходи до іменування.

Правила дизайну: іменування та стандарти

Дотепер ми розбирали структуру HTTP API. Тепер переходимо до стилістики — як називати поля, форматувати дати, передавати гроші та дробові числа. Ці правила не зафіксовані в жодному RFC, але від їхнього дотримання залежить, чи буде ваш API зрозумілим і безпечним.

1. Принцип: Явне краще за неявне

Головний принцип дизайну API: якщо з інтерфейсу не зрозуміло, що саме означає поле, параметр чи відповідь — його потрібно перейменувати або доповнити.

Порівняйте два варіанти:

{
  "time": 1708934400,
  "value": 10.5,
  "status": 1
}

Що таке time? Час створення? Оновлення? Доставки? value — це що? Ціна? Об'єм? Рейтинг? status: 1 — 1 означає «активний»? «Створений»? «Помилка»?

Правила явного іменування

Ім'я поля повинно однозначно описувати його зміст

// ❌ Неоднозначне
app.MapGet("/v1/orders/{id}", (int id) =>
    Results.Ok(new
    {
        id,
        time = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
        value = 10.5,
        type = 1
    }));

// ✅ Самодокументоване
app.MapGet("/v1/orders/{id}", (int id) =>
    Results.Ok(new
    {
        id,
        created_at = "2024-02-26T14:00:00Z",
        price = "10.50",
        currency = "UAH",
        status = "processing"
    }));

Булеві поля: is_, has_, can_

{
  "is_active": true,
  "has_discount": false,
  "can_be_cancelled": true
}

Колекції: завжди множина

{
  "orders": [...],
  "items": [...],
  "attachments": [...]
}

Уникайте абревіатур

❌ qty, amt, desc, addr, cfg, msg
✅ quantity, amount, description, address, config, message

2. Стандарти форматів даних

2.1. Дата і час: ISO 8601

Ніколи не передавайте дату як Unix-timestamp, рядок у довільному форматі або окремі поля (рік, місяць, день). Використовуйте ISO 8601 — єдиний міжнародний стандарт.
Формат:           2024-02-26T14:30:00Z
З часовою зоною:  2024-02-26T16:30:00+02:00
Тільки дата:      2024-02-26
Тільки час:       14:30:00
Тривалість:       P1DT12H30M (1 день 12 годин 30 хв)
ISO 8601 у Minimal API
app.MapGet("/v1/orders/{id}", (int id) =>
{
    var order = new
    {
        id,
        // ISO 8601 з UTC — завжди "Z" в кінці
        created_at = DateTime.UtcNow
            .ToString("o"), // "2024-02-26T14:30:00.0000000Z"
        
        // Або DateTimeOffset для зони
        updated_at = DateTimeOffset.Now
            .ToString("o"), // "2024-02-26T16:30:00+02:00"
        
        // Тривалість
        estimated_preparation = "PT5M30S" // 5 хв 30 сек
    };
    return Results.Ok(order);
});
Порада: ASP.NET Core за замовчуванням серіалізує DateTime та DateTimeOffset у формат ISO 8601. Використовуйте DateTime.UtcNow (а не DateTime.Now) для API, щоб уникнути плутанини з часовими зонами.

2.2. Грошові суми: рядок або цілі числа

Критична помилка: передача грошових сум як double або float. Через обмеження IEEE 754 (floating point), 0.1 + 0.2 = 0.30000000000000004.
{
  "price": 10.5,
  "total": 0.30000000000000004
}

Втрата точності! У фінансових розрахунках — катастрофа.

Грошові суми у Minimal API
app.MapPost("/v1/orders", (OrderRequest req) =>
{
    // Ціна — рядок, парсимо у decimal
    // decimal має 28-29 значущих цифр
    if (!decimal.TryParse(req.Price, out var price))
        return Results.BadRequest(
            new { error = "Invalid price format" });

    // Або використовуємо мінімальні одиниці
    var priceMinorUnits = (int)(price * 100);

    return Results.Created("/v1/orders/42", new
    {
        id = 42,
        price = req.Price,        // "10.50" — рядок
        currency = req.Currency,  // "UAH"
        price_minor_units = priceMinorUnits // 1050
    });
});

record OrderRequest(string Price, string Currency);

2.3. Валюта: ISO 4217

Для позначення валюти використовуйте ISO 4217 — тризначний код:

КодВалюта
UAHУкраїнська гривня
USDДолар США
EURЄвро
GBPФунт стерлінгів
{
  "price": "10.50",
  "currency": "UAH"
}
Не кодуйте валюту як число (1 = UAH, 2 = USD) або скорочення (грн, $). Використовуйте тільки ISO 4217.

2.4. Мова та локалізація: ISO 639 та IETF BCP 47

{
  "language": "uk",
  "locale": "uk-UA"
}

Заголовок Accept-Language дозволяє клієнту вказати бажану мову:

GET /v1/menu HTTP/1.1
Accept-Language: uk-UA, uk;q=0.9, en;q=0.5

2.5. Країна: ISO 3166-1 alpha-2

{
  "country": "UA",
  "phone_prefix": "+380"
}

3. Стандарти іменування

3.1. URL: kebab-case

✅ /v1/coffee-machines/{id}
✅ /v1/long-term-orders/{id}
❌ /v1/coffeeMachines/{id}
❌ /v1/coffee_machines/{id}

3.2. Query-параметри: snake_case

✅ /v1/orders?user_id=42&sort_by=created_at
❌ /v1/orders?userId=42&sortBy=createdAt

3.3. JSON-тіло: snake_case або camelCase

{
  "user_id": 42,
  "first_name": "Олексій",
  "coffee_machine_id": 123
}

Перевага: легко переносити параметр з query у тіло й навпаки.

Налаштування camelCase у ASP.NET Core
var builder = WebApplication.CreateBuilder(args);

// За замовчуванням ASP.NET Core
// вже використовує camelCase
builder.Services.ConfigureHttpJsonOptions(options =>
{
    // Якщо потрібен snake_case:
    options.SerializerOptions.PropertyNamingPolicy = 
        System.Text.Json.JsonNamingPolicy.SnakeCaseLower;
});

var app = builder.Build();

app.MapGet("/v1/orders/{id}", (int id) =>
    Results.Ok(new OrderResponse(
        id, 
        "lungo", 
        "10.50", 
        "UAH")));

app.Run();

// C# використовує PascalCase для властивостей
// але JSON серіалізується у обраний кейсинг
record OrderResponse(
    int Id,
    string Recipe,
    string Price,
    string Currency);
// → camelCase: {"id":1,"recipe":"lungo","price":"10.50","currency":"UAH"}
// → snake_case: {"id":1,"recipe":"lungo","price":"10.50","currency":"UAH"}

3.4. Заголовки: Train-Case

✅ Content-Type, Authorization, Accept-Language
✅ X-CoffeeAPI-Request-Id (кастомний)

Зведена таблиця

ДеКейсингПриклад
URL pathkebab-case/v1/coffee-machines
Query paramssnake_case?user_id=42
JSON bodysnake_case або camelCase"user_id" або "userId"
ЗаголовкиTrain-CaseContent-Type
Доменlowercaseapi.coffee-service.com

4. Принцип консистентності

Консистентність — найважливіший принцип дизайну API. Обрали одну конвенцію — дотримуйтесь її всюди:

Одна сутність — одне ім'я

Якщо замовлення називається order в одному місці, не називайте його purchase, request або booking в іншому.

Однакова структура відповідей

Якщо список повертає { items: [...], total: N }, то всі списки мають повертати таку саму структуру.

Однакові поля — однаковий тип

Якщо id — число в одному ендпоінті, воно має бути числом всюди. Не "id": 42 в одному місці і "id": "42" в іншому.

Одна помилка — одна структура

Формат помилки ({ error, message, details }) однаковий для всіх ендпоінтів, незалежно від типу помилки.
Консистентна структура відповідей
// ✅ Консистентна «обгортка» для всіх списків
app.MapGet("/v1/orders", (int? limit, string? cursor) =>
{
    var orders = db.GetOrders(limit ?? 20, cursor);
    return Results.Ok(new ListResponse<Order>(
        orders.Items, orders.Total, orders.NextCursor));
});

app.MapGet("/v1/recipes", (int? limit, string? cursor) =>
{
    var recipes = db.GetRecipes(limit ?? 20, cursor);
    return Results.Ok(new ListResponse<Recipe>(
        recipes.Items, recipes.Total, recipes.NextCursor));
});

// Єдина структура для всіх списків
record ListResponse<T>(
    IEnumerable<T> Items,
    int Total,
    string? NextCursor);

5. Стандартні поля кожного ресурсу

Кожен ресурс у вашому API повинен мати набір стандартних полів:

{
  "id": 42,
  "created_at": "2024-02-26T14:00:00Z",
  "updated_at": "2024-02-26T16:30:00Z",

  "recipe": "lungo",
  "status": "processing"
}
ПолеТипОпис
idint чи string (UUID)Унікальний ідентифікатор
created_atISO 8601Час створення
updated_atISO 8601Час останнього оновлення
Порада: Якщо ресурс може бути видалений — додайте deleted_at: null | ISO 8601 для soft delete. Якщо ресурс має автора — created_by: int.

6. Перерахування (enum): рядки, а не числа

{
  "status": 1,
  "type": 3,
  "priority": 2
}

Що означає status: 1? Без документації — нічого.

Enum як рядки у ASP.NET Core
var builder = WebApplication.CreateBuilder(args);

builder.Services.ConfigureHttpJsonOptions(options =>
{
    // Серіалізувати enum як рядки
    options.SerializerOptions.Converters.Add(
        new System.Text.Json.Serialization
            .JsonStringEnumConverter());
});

var app = builder.Build();

app.MapGet("/v1/orders/{id}", (int id) =>
    Results.Ok(new
    {
        id,
        status = OrderStatus.Processing,
        priority = Priority.High
    }));
// → {"id":1,"status":"Processing","priority":"High"}

app.Run();

enum OrderStatus { Created, Processing, Completed, Cancelled }
enum Priority { Low, Medium, High }

7. Практичні завдання

Рівень 1: Рефакторинг

Рівень 2: Проєктування


8. Резюме

Явне > Неявне

Кожне поле самодокументоване. Ніяких time, value, type без контексту. Булеві — з is_/has_/can_.

Стандарти форматів

Дати: ISO 8601. Гроші: рядок або мінімальні одиниці. Валюта: ISO 4217. Enum: рядки, не числа.

Кейсинг за конвенцією

URL: kebab-case. Query: snake_case. JSON: snake_case або camelCase (обрати одне!). Заголовки: Train-Case.

Консистентність

Одна сутність — одне ім'я. Одна структура для списків. Однакові типи полів у всіх ендпоінтах.

Далі: у наступній статті ми розберемо валідацію, ліміти та обробку помилок — технічні обмеження, валідацію вхідних даних та формування інформативних повідомлень про помилки.

Copyright © 2026