API

HTTP-методи, статус-коди та заголовки

Складові HTTP-запитів: URL-адресація, HTTP-методи (GET, POST, PUT, DELETE, PATCH), їх семантика та ідемпотентність, заголовки, статус-коди 1xx–5xx та антипатерни.

HTTP-методи, статус-коди та заголовки

У попередніх статтях ми зрозуміли навіщо існує HTTP API. Тепер час розібрати з чого складаються HTTP-запити і відповіді — це фундамент, на якому будується весь дизайн API.

1. Анатомія HTTP-запиту

HTTP-запит — це структура, що складається з чотирьох основних частин:

Приклад HTTP-запиту
POST /v1/orders HTTP/1.1
Host: api.coffee-service.com
Content-Type: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...

{
  "coffee_machine_id": 123,
  "recipe": "lungo",
  "volume": "300ml",
  "price": "10.23",
  "currency": "UAH"
}
ЧастинаПрикладОпис
Метод (дієслово)POSTОперація, що застосовується до ресурсу
URL (адреса)/v1/ordersІдентифікатор ресурсу
ЗаголовкиContent-Type: application/jsonМетаінформація запиту
Тіло{"recipe": "lungo"}Дані для обробки (опціонально)

Відповідь сервера має аналогічну структуру:

Приклад HTTP-відповіді
HTTP/1.1 201 Created
Location: /v1/orders/42
Content-Type: application/json

{
  "id": 42,
  "status": "processing"
}
ЧастинаПрикладОпис
Статус-код201 CreatedМашиночитаємий результат операції
ЗаголовкиLocation: /v1/orders/42Метаінформація відповіді
Тіло{"id": 42}Дані результату (опціонально)

2. URL — система адресації

URL (Uniform Resource Locator) — це одиниця адресації у HTTP API. Передбачається, що API має використовувати систему адрес настільки ж гранулярну, як і предметна область — тобто у будь-якої сутності, якою ми маємо маніпулювати незалежно, має бути свій URL.

Компоненти URL

https://api.coffee-service.com:443/v1/orders?status=active&limit=10#results
  │            │                 │     │           │                  │
схема        хост              порт  шлях       запит             фрагмент
(scheme)    (host)            (port) (path)    (query)           (fragment)
Схема (scheme)
string
Протокол звернення. Для API завжди https:. HTTP без шифрування (http:) використовується тільки для локальної розробки.
Хост (host)
string
Найбільша одиниця адресації. Може включати піддомени (api.example.com). Нечутливий до регістру.
Порт (port)
number
Опціональний. За замовчуванням 443 для HTTPS, 80 для HTTP. Kestrel зазвичай слухає на 5000/5001.
Шлях (path)
string
Частина URL між хостом і символом ?. Описує ієрархію ресурсів. Чутливий до регістру.
Запит (query)
string
Після символу ?. Пари ключ=значення, розділені &. Використовується для фільтрів, пагінації, нестрогих ієрархій.
Фрагмент (fragment)
string
Після символу #. Адресація всередині документа. Зазвичай ігнорується сервером.

Path vs Query: строга та нестрога ієрархія

Шлях описує строгу підпорядкованість ресурсів:

// Кавова машина належить конкретному закладу
app.MapGet("/v1/places/{placeId}/coffee-machines/{machineId}", 
    (int placeId, int machineId) =>
{
    return Results.Ok(new 
    { 
        id = machineId, 
        place_id = placeId,
        model = "DeLonghi ECAM 350.75" 
    });
});

URL: /v1/places/5/coffee-machines/12 — машина 12, що належить закладу 5.

Правило з книги Константинова: Частини path описують строгу ієрархію підпорядкування ресурсів (кавова машина належить конкретному закладу). Через query виражаються нестрогі ієрархії та параметри операцій (пошук за координатами).

3. HTTP-методи (дієслова)

HTTP-метод визначає, яку операцію застосувати до ресурсу. RFC 9110 стандартизує вісім методів, з яких п'ять найважливіших для API:

Таблиця семантики

МетодСемантикаБезпечнийІдемпотентнийМає тіло
GETПовертає представлення ресурсу✅ Так✅ Так❌ Не рекомендовано
POSTОбробляє запит згідно зі своїм внутрішнім устроєм❌ Ні❌ Ні✅ Так
PUTПовністю замінює ресурс даними з тіла запиту❌ Ні✅ Так✅ Так
DELETEВидаляє ресурс❌ Ні✅ Так❌ Не рекомендовано
PATCHЧастково модифікує ресурс❌ Ні❌ Ні✅ Так
Поширена помилка: метод POST призначений тільки для створення ресурсів. Це не так! Створення — лише один з варіантів «обробки запиту згідно зі своїм внутрішнім устроєм». POST також використовується для складних пошуків, операцій, що не вписуються у CRUD, batch-запитів тощо.

Два критичні поняття

Безпечність (Safety)

Безпечний метод — той, що не модифікує стан ресурсу. Тільки GET є безпечним. Це означає:

  • Відповідь на GET можна кешувати
  • Проміжні агенти (CDN, проксі) мають право повторити GET-запит без ризику
  • Клієнт може виконати GET без побоювань «зламати» дані

Ідемпотентність (Idempotency)

Ідемпотентний метод — той, де повторне виконання того самого запиту залишає систему у тому самому стані.

Ідемпотентність на прикладі
var app = WebApplication.Create(args);

// PUT — ідемпотентний: повторний виклик
// не змінює результат
app.MapPut("/v1/orders/{id}", (int id, OrderUpdate update) =>
{
    // Перший виклик: рецепт стає "americano"
    // Другий виклик: рецепт залишається "americano"
    // Результат той самий!
    return Results.Ok(new { id, recipe = update.Recipe });
});

// POST — НЕ ідемпотентний: кожен виклик
// створює нове замовлення
app.MapPost("/v1/orders", (OrderRequest req) =>
{
    // Перший виклик: створено замовлення #42
    // Другий виклик: створено замовлення #43 (!)
    return Results.Created("/v1/orders/42", new { id = 42 });
});

app.Run();

record OrderUpdate(string Recipe);
record OrderRequest(string Recipe, string CoffeeMachineId);
Ключовий інсайт з книги: URL запит — це ключ ідемпотентності для PUT та DELETE. PUT /v1/orders/42 повністю перезаписує ресурс за адресою /v1/orders/42. Повторний виклик не змінить результат. DELETE /v1/orders/42 видаляє ресурс — повторний виклик не видалить нічого нового.

Симетричність GET / PUT / DELETE

Методи GET, PUT та DELETE повинні бути симетричними відносно одного URL. Це означає, що всі три методи працюють із одним і тим самим ресурсом за однією адресою, і результат одного методу має бути консистентним з результатами інших.

Аналогія: уявіть комірку (locker) з номером 42. На ній написана адреса — /v1/orders/42. Ви можете:

  • PUT — покласти в комірку нові речі (старі замінюються повністю)
  • GET — відкрити комірку та подивитись, що там лежить
  • DELETE — очистити комірку

Логічно, що якщо ви поклали щось у комірку (PUT), а потім одразу відкрили її (GET) — ви побачите саме те, що щойно поклали. А якщо ви очистили комірку (DELETE), а потім спробували подивитись (GET) — комірка порожня (404).

Правила симетричності:

Послідовність дійОчікуваний результат
PUT /urlGET /urlGET повертає те саме, що було передано в PUT
DELETE /urlGET /urlGET повертає 404 Not Found або 410 Gone
PUT /urlDELETE /urlGET /urlGET повертає 404 (ресурс видалено)
PUT /urlPUT /url (повтор)Стан ресурсу не змінюється (ідемпотентність)
DELETE /urlDELETE /url (повтор)Стан системи не змінюється (ідемпотентність)

Тепер подивимось, як це виглядає крок за кроком:

PUT створює/оновлює ресурс

PUT /v1/orders/42 HTTP/1.1
Content-Type: application/json

{"recipe": "americano", "volume": "350ml"}

GET повертає те, що було записано в PUT

GET /v1/orders/42 HTTP/1.1
HTTP/1.1 200 OK
{"id": 42, "recipe": "americano", "volume": "350ml"}

Зверніть увагу: GET повернув ті самі дані, що ми передали у PUT (з точністю до полів за замовчуванням, як-от id).

DELETE видаляє ресурс

DELETE /v1/orders/42 HTTP/1.1
HTTP/1.1 204 No Content

GET після DELETE повертає 404

GET /v1/orders/42 HTTP/1.1
HTTP/1.1 404 Not Found

Ресурс видалений — GET це підтверджує. Саме так працює симетричність.

Симетричність у Minimal API
var app = WebApplication.Create(args);
var orders = new Dictionary<int, Order>();

app.MapPut("/v1/orders/{id}", (int id, Order order) =>
{
    orders[id] = order with { Id = id };
    return Results.Ok(orders[id]);
});

app.MapGet("/v1/orders/{id}", (int id) =>
{
    return orders.TryGetValue(id, out var order)
        ? Results.Ok(order)
        : Results.NotFound();
});

app.MapDelete("/v1/orders/{id}", (int id) =>
{
    orders.Remove(id);
    return Results.NoContent();
});

app.Run();

record Order(int Id, string Recipe, string Volume);

4. Антипатерни використання HTTP-методів

Найчастіші антипатерни з книги Константинова — помилки, які призводять до реальних інцидентів у production:

❌ Модифікуючі операції за GET

// ❌ АНТИПАТЕРН: GET не повинен змінювати стан!
app.MapGet("/v1/orders/{id}/cancel", (int id) =>
{
    // Проблема 1: проміжний проксі або кеш
    //   може повторити цей запит автоматично
    // Проблема 2: соціальні мережі виконують
    //   GET-запити для генерації прев'ю посилань
    // Проблема 3: пошукові роботи "скасують"
    //   ваші замовлення, обходячи URL
    return Results.Ok(new { cancelled = true });
});

// ✅ ПРАВИЛЬНО: модифікація через POST/DELETE
app.MapDelete("/v1/orders/{id}", (int id) =>
{
    return Results.NoContent();
});

❌ Неідемпотентні операції за PUT/DELETE

// ❌ АНТИПАТЕРН: PUT повинен бути ідемпотентним
app.MapPut("/v1/orders/{id}/add-item", 
    (int id, OrderItem item) =>
{
    // Кожен виклик ДОДАЄ елемент → не ідемпотентно!
    // Якщо фреймворк повторить запит при таймауті,
    // елемент додасться двічі
    return Results.Ok();
});

// ✅ ПРАВИЛЬНО: PUT замінює ресурс цілком
app.MapPut("/v1/orders/{id}", (int id, Order order) =>
{
    // Повна заміна — повторний виклик не змінює стан
    return Results.Ok(order);
});

❌ Тіло у GET або DELETE

// ❌ АНТИПАТЕРН: GET з тілом запиту
// Будь-який мережевий агент має право
// проігнорувати тіло GET-запиту
app.MapGet("/v1/orders", (HttpContext ctx) =>
{
    // Деякі проксі просто відкинуть тіло!
    return Results.Ok();
});

// ✅ ПРАВИЛЬНО: параметри через query
app.MapGet("/v1/orders", (string? status, int? limit) =>
{
    return Results.Ok(new { status, limit });
});

5. Заголовки (Headers)

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

Обов'язкові та типові заголовки

ЗаголовокОбов'язковийПризначення
Host✅ ТакДоменне ім'я сервера
Content-TypeФактично такФормат тіла (application/json)
Content-LengthРекомендованоРозмір тіла (деякі проксі не пропускають без нього)
AcceptРекомендованоФормати, які клієнт розуміє
Accept-EncodingРекомендованоАлгоритми стиснення (gzip, br)
AuthorizationДля захищених ресурсівТокен авторизації (Bearer ...)
DateРекомендованоЧас запиту/відповіді

Кастомні заголовки: префікс X-

Якщо ваш API передає додаткові метадані через заголовки, існує ризик колізії з існуючими стандартними заголовками. Книга Константинова рекомендує подвійний префікс:

GET /v1/orders HTTP/1.1
Host: api.coffee-service.com
X-CoffeeAPI-Request-Id: req-abc-123
X-CoffeeAPI-Partner-Id: partner-42
Рекомендація з книги: Використовуйте формат X-ApiName-FieldName. Префікс X- для читабельності (відрізняє спеціальні заголовки від стандартних), а префікс з ім'ям API — щоб не трапились колізії з іншими нестандартними заголовками.

Content Negotiation через заголовки

Заголовки Accept* та Content-Type використовуються для узгодження форматів:

Content Negotiation у Minimal API
app.MapGet("/v1/orders/{id}", (int id, HttpContext ctx) =>
{
    var accept = ctx.Request.Headers.Accept
        .FirstOrDefault() ?? "application/json";

    // Перевірка стиснення, яке розуміє клієнт
    var encoding = ctx.Request.Headers.AcceptEncoding
        .FirstOrDefault();

    // Мова, якій віддає перевагу клієнт
    var language = ctx.Request.Headers.AcceptLanguage
        .FirstOrDefault();

    return Results.Ok(new 
    { 
        id, 
        recipe = "lungo",
        negotiated_format = accept 
    });
});

6. Статус-коди

Статус-код — це машиночитаємий опис результату HTTP-запиту у вигляді тризначного числа. Усі коди діляться на п'ять груп:

2xx — Успіх

КодНазваКоли використовувати
200 OKУспішна операціяGET, PUT, PATCH — повернення даних
201 CreatedРесурс створеноPOST — створення нового ресурсу
204 No ContentУспіх без тілаDELETE — видалення ресурсу
Правильне використання 2xx
// 200 OK — отримання ресурсу
app.MapGet("/v1/orders/{id}", (int id) =>
    Results.Ok(new { id, recipe = "lungo" }));

// 201 Created — створення ресурсу
app.MapPost("/v1/orders", (OrderRequest req) =>
    Results.Created($"/v1/orders/42", new { id = 42 }));

// 204 No Content — видалення без тіла відповіді
app.MapDelete("/v1/orders/{id}", (int id) =>
    Results.NoContent());

3xx — Перенаправлення

КодНазваКоли використовувати
301 Moved PermanentlyРесурс переміщено назавждиЗміна URL API
304 Not ModifiedДані не змінилисьВідповідь на умовний запит (If-None-Match)
307 Temporary RedirectТимчасове перенаправленняЗберігає метод запиту (на відміну від 301)

4xx — Клієнтські помилки

КодНазваТиповий сценарій
400 Bad RequestНевалідний запитНеправильний JSON, відсутнє обов'язкове поле
401 UnauthorizedНе аутентифікованоВідсутній або невалідний токен
403 ForbiddenНемає правТокен валідний, але дія заборонена
404 Not FoundРесурс не знайденоНеіснуючий ID або URL
405 Method Not AllowedМетод не підтримуєтьсяDELETE на ресурсі, що не можна видалити
409 ConflictКонфлікт стануСпроба створити дублікат
422 Unprocessable EntityСемантична помилкаJSON валідний, але дані не мають сенсу
429 Too Many RequestsЗанадто багато запитівRate limiting
Правильне використання 4xx
app.MapPost("/v1/orders", (OrderRequest? req) =>
{
    // 400 — невалідний запит
    if (req is null)
        return Results.BadRequest(new 
        { 
            error = "Request body is required" 
        });

    // 422 — JSON правильний, але дані некоректні
    if (string.IsNullOrEmpty(req.Recipe))
        return Results.UnprocessableEntity(new 
        { 
            error = "Recipe is required",
            field = "recipe" 
        });

    // 201 — все добре
    return Results.Created($"/v1/orders/42", 
        new { id = 42 });
});

5xx — Серверні помилки

КодНазваТиповий сценарій
500 Internal Server ErrorВнутрішня помилкаНеперехоплений виняток
502 Bad GatewayПроксі не отримав відповідьМікросервіс недоступний
503 Service UnavailableСервіс тимчасово недоступнийПеревантаження, обслуговування
504 Gateway TimeoutТаймаут проксіМікросервіс відповідає занадто довго
Критичне зауваження з книги: якщо клієнт отримав невідомий код xyz, згідно зі специфікацією він зобов'язаний виконати ту дію, яку виконав би при отриманні коду x00. Тобто невідомий 493 обробляється як 400 Bad Request. Це означає, що першу цифру коду завжди потрібно обирати правильно.

Пастка кешування: 404

Реальний інцидент з книги: деякі 4xx коди кешуються за замовчуванням — зокрема 404, 405, 410, 414. Розробники часто не знають про це. Випадкове видалення налаштувань (ендпоінт почав повертати 404) призвело до неработоспроможності сервісу протягом кількох годин, оскільки клієнти кешували 404 і просто не запитували нову версію даних.Рішення: завжди явно вказуйте заголовки кешування (Cache-Control: no-store) для помилок, якщо не впевнені у поведінці.

7. Кейсинг: Вічна проблема

Різні частини HTTP-запиту використовують різний кейсинг — і це створює одну з найбільших неконсистентностей у HTTP API. Розглянемо, чому це так і як з цим жити.

Що таке кейсинг

Кейсинг (casing) — це спосіб запису складених слів. В API зустрічаються чотири основних стилі:

СтильПрикладДе використовується
camelCasecoffeeMachineIdJavaScript, JSON (за конвенцією JS)
PascalCaseCoffeeMachineIdC# властивості, .NET
snake_casecoffee_machine_idPython, Ruby, query-параметри
kebab-casecoffee-machine-idURL path, домени, CSS

Таблиця кейсингу в HTTP

Частина запитуКейсингПрикладЧутливість до регістру
Доменkebab-caseapi.coffee-service.com❌ Ні
Заголовки (імена)Train-CaseContent-Type, Accept-Language❌ Ні
Заголовки (значення)Залежитьapplication/json — ні, ETag — так⚠️ По-різному
Шлях (path)kebab-case/v1/coffee-machines✅ Так
Querysnake_case?machine_id=123✅ Так
JSON тілоsnake_case або camelCase{"machine_id": 123}✅ Так

Проблема: міграція параметра між частинами запиту

Один і той самий параметр може знаходитись у різних частинах запиту. Наприклад, ідентифікатор партнера:

1. У піддомені:    partner-42.api.service.com
2. У path:         /v1/partner-42/orders
3. У query:        /v1/orders?partner_id=42
4. У заголовку:    X-CoffeeAPI-Partner-Id: 42
5. У JSON тілі:    {"partner_id": 42}

Зверніть увагу: в кожній частині запиту параметр записується по-різному! І це не помилка — це норма HTTP, бо:

  • Домен не підтримує символ _ (underscore) і заглавні літери → тільки kebab-case
  • Path чутливий до регістру, але конвенція — kebab-case
  • Query за конвенцією — snake_case
  • ЗаголовкиTrain-Case (кожне слово з великої літери)
  • JSONsnake_case або camelCase (залежить від обраної конвенції)
З книги Константинова: Ситуація з кейсингом настільки погана і заплутана, що консистентного та зручного рішення просто не існує. Рекомендація книги: давайте токени в тому кейсингу, який є загальноприйнятим для тієї секції запиту, де знаходиться токен. Якщо положення токена змінюється — змінюється і кейсинг.

Практична порада: snake_case для query і JSON

snake_case у query та JSON має практичну перевагу: параметр легко переносити між query і тілом без зміни імені. Це найчастіший сценарій при розробці API:

// Сьогодні параметр у query:
GET /v1/orders?user_id=42&sort_by=created_at

// Завтра вирішили перенести у тіло POST:
POST /v1/orders/search
{"user_id": 42, "sort_by": "created_at"}
// Ті самі імена — нічого не зламається!

Якщо б ви використовували camelCase у JSON і snake_case у query — при переносі параметра довелось би перейменовувати user_iduserId, що створює плутанину.

Налаштування кейсингу в ASP.NET Core

Конфігурація JSON-серіалізації
var builder = WebApplication.CreateBuilder(args);

builder.Services.ConfigureHttpJsonOptions(options =>
{
    // За замовчуванням ASP.NET Core серіалізує
    // PascalCase (C#) → camelCase (JSON)
    // CoffeeMachineId → coffeeMachineId

    // Якщо потрібен snake_case:
    options.SerializerOptions.PropertyNamingPolicy =
        System.Text.Json.JsonNamingPolicy.SnakeCaseLower;
    // CoffeeMachineId → coffee_machine_id
});

var app = builder.Build();
app.MapGet("/v1/orders/{id}", (int id) =>
    Results.Ok(new OrderResponse(id, "lungo", 42)));
app.Run();

record OrderResponse(
    int Id,                // → "id" (обидва кейсинги)
    string RecipeName,     // → "recipeName" (camel)
                           //   "recipe_name" (snake)
    int CoffeeMachineId    // → "coffeeMachineId" (camel)
                           //   "coffee_machine_id" (snake)
);
Порада: Оберіть один кейсинг для JSON на початку проєкту і дотримуйтесь його всюди. Змінити кейсинг пізніше — це breaking change для всіх клієнтів.

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

Рівень 1: Базовий

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


9. Резюме

URL = адреса ресурсу

Path для строгої ієрархії ресурсів, query для фільтрів і параметрів. Кожна незалежна сутність має свій URL.

5 методів для всього

GET (читати), POST (створити/обробити), PUT (замінити), DELETE (видалити), PATCH (частково оновити). Знайте їх семантику, безпечність та ідемпотентність.

Статус-код = машиночитаємий результат

Перша цифра завжди правильна (2xx/4xx/5xx). Невідомий код xyz обробляється як x00. Пам'ятайте про кешування 404.

Заголовки = метадані

Читаються до тіла, використовуються проміжними агентами. Кастомні заголовки — з подвійним префіксом X-ApiName-.

Далі: у наступній статті ми розберемо, як організувати HTTP API за принципами REST — stateless-дизайн, декомпозиція на мікросервіси, кешування через ETag, та JWT-авторизація.

Copyright © 2026