HTTP-методи, статус-коди та заголовки
HTTP-методи, статус-коди та заголовки
1. Анатомія 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/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)
https:. HTTP без шифрування (http:) використовується тільки для локальної розробки.api.example.com). Нечутливий до регістру.?. Описує ієрархію ресурсів. Чутливий до регістру.?. Пари ключ=значення, розділені &. Використовується для фільтрів, пагінації, нестрогих ієрархій.#. Адресація всередині документа. Зазвичай ігнорується сервером.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.
Query використовується для фільтрів та параметрів операцій:
// Пошук пропозицій за координатами
app.MapGet("/v1/search",
(double? lat, double? lng, int limit) =>
{
return Results.Ok(new
{
results = new[]
{
new { place = "Кавомашина на Хрещатику" }
},
total = 1
});
});
URL: /v1/search?lat=50.45&lng=30.52&limit=10 — координати і ліміт не є ієрархією.
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);
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 /url → GET /url | GET повертає те саме, що було передано в PUT |
DELETE /url → GET /url | GET повертає 404 Not Found або 410 Gone |
PUT /url → DELETE /url → GET /url | GET повертає 404 (ресурс видалено) |
PUT /url → PUT /url (повтор) | Стан ресурсу не змінюється (ідемпотентність) |
DELETE /url → DELETE /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 це підтверджує. Саме так працює симетричність.
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-методів
❌ Модифікуючі операції за 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 використовуються для узгодження форматів:
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 — видалення ресурсу |
// 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 |
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 зустрічаються чотири основних стилі:
| Стиль | Приклад | Де використовується |
|---|---|---|
camelCase | coffeeMachineId | JavaScript, JSON (за конвенцією JS) |
PascalCase | CoffeeMachineId | C# властивості, .NET |
snake_case | coffee_machine_id | Python, Ruby, query-параметри |
kebab-case | coffee-machine-id | URL path, домени, CSS |
Таблиця кейсингу в HTTP
| Частина запиту | Кейсинг | Приклад | Чутливість до регістру |
|---|---|---|---|
| Домен | kebab-case | api.coffee-service.com | ❌ Ні |
| Заголовки (імена) | Train-Case | Content-Type, Accept-Language | ❌ Ні |
| Заголовки (значення) | Залежить | application/json — ні, ETag — так | ⚠️ По-різному |
| Шлях (path) | kebab-case | /v1/coffee-machines | ✅ Так |
| Query | snake_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(кожне слово з великої літери) - JSON —
snake_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_id → userId, що створює плутанину.
Налаштування кейсингу в ASP.NET Core
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)
);
8. Практичні завдання
Рівень 1: Базовий
Для кожної операції визначте правильний HTTP-метод та очікуваний статус-код відповіді:
- Отримати список усіх рецептів
- Створити нове замовлення
- Повністю замінити профіль користувача
- Видалити замовлення
- Змінити лише email у профілі
- Перевірити, чи існує замовлення (без отримання даних)
У наступному API знайдіть усі антипатерни та виправте їх:
app.MapGet("/api/deleteOrder", (int id) => ...);
app.MapPost("/api/getUsers", () => ...);
app.MapPut("/api/addItemToOrder",
(int orderId, Item item) => ...);
app.MapDelete("/api/orders/{id}",
(int id, DeleteRequest body) => ...);
Рівень 2: Проєктування
Для API бібліотеки спроєктуйте URL-структуру, використовуючи правильні HTTP-методи. Ресурси: книги, автори, читачі, бронювання. Вимоги:
- Книга належить автору (строга ієрархія)
- Пошук книг за жанром (нестрога ієрархія)
- Читач може забронювати книгу
- Адміністратор може видалити бронювання
Напишіть Minimal API маршрути для всіх операцій.
Напишіть Minimal API ендпоінт POST /v1/orders, який обробляє всі можливі сценарії з правильними статус-кодами:
- Успішне створення →
201 - Відсутнє тіло запиту →
400 - Невалідний рецепт →
422 - Кавова машина не знайдена →
404 - Замовлення вже існує (дублікат) →
409 - Внутрішня помилка →
500
9. Резюме
URL = адреса ресурсу
5 методів для всього
Статус-код = машиночитаємий результат
xyz обробляється як x00. Пам'ятайте про кешування 404.Заголовки = метадані
X-ApiName-.Далі: у наступній статті ми розберемо, як організувати HTTP API за принципами REST — stateless-дизайн, декомпозиція на мікросервіси, кешування через ETag, та JWT-авторизація.
Парадигми API та концепція REST
Еволюція від RPC до REST. Шість обмежень Філдінга. Чому термін «REST API» неточний. Порівняння HTTP API, gRPC та GraphQL.
Організація HTTP API за принципами REST
Stateless-дизайн, декомпозиція на мікросервіси, кешування через ETag, умовні запити (If-None-Match, If-Match), JWT-авторизація та оптимістичне керування паралелізмом.