api-design
Проектирование HTTP API — REST-конвенции (ресурсы, методы, статус-коды), пагинация (cursor vs offset), версионирование, идемпотентность (Idempotency-Key), формат ошибок RFC 7807 (problem+json), безопа
SKILL.mdskills/api-design/SKILL.md
GitHubНавык: API Design
REST-конвенции
Ресурсы — существительные во множественном числе; действие выражает HTTP-метод, не URL (POST /orders, а не POST /createOrder). Вложенность — максимум один уровень (/users/{id}/orders), глубже — фильтром (/orders?userId=). Действие вне CRUD — суб-ресурсом: POST /orders/{id}/cancel.
| Метод | Семантика | Идемпотентен | Успех | Типичные ошибки |
|---|---|---|---|---|
| GET | чтение | ✅ | 200 | 404 |
| POST | создание / действие | ❌ | 201 (+ Location) или 200 |
400, 409, 422 |
| PUT | полная замена | ✅ | 200 | 404, 409 |
| PATCH | частичное обновление | ❌ | 200 | 404, 409, 422 |
| DELETE | удаление | ✅ | 204 | 404 |
Статусы ошибок: 400 — синтаксически кривой запрос; 422 — валидный JSON, не прошедший бизнес-валидацию; 401 — не аутентифицирован; 403 — аутентифицирован, но нельзя; 409 — конфликт состояния (дубликат, устаревшая версия); 429 — превышен лимит (+ Retry-After).
Пагинация
Offset (?page=3&limit=20) |
Cursor (?cursor=xyz&limit=20) |
|
|---|---|---|
| Простота | ✅ проще, произвольная страница | сложнее, только «дальше» |
| Стабильность при вставках/удалениях | ❌ дубли и пропуски между страницами | ✅ стабильна |
| Скорость на глубине | ❌ OFFSET 100000 читает и выбрасывает |
✅ WHERE (sort_key, id) < cursor по индексу |
| Когда | админки, маленькие статичные списки | ленты, публичные API, большие/живые данные |
Cursor — непрозрачная строка (base64 от (sort_key, id)), клиент её не парсит. Ответ списка — конверт: { "data": […], "nextCursor": "…", "hasMore": true }. Лимит — с дефолтом и максимумом (например 20/100).
Версионирование
- Ломающее (удаление/переименование поля, смена типа или семантики) — только в новой версии. Добавление опциональных полей — не ломающее, версии не требует.
- Дефолт — версия в пути:
/v1/…(явно, кэшируемо, просто в роутинге). Альтернатива — заголовок (Accept: …;version=2) для чистых URL. - Клиент — tolerant reader: неизвестные поля ответа игнорирует, тогда добавления безопасны.
- Старую версию не бросай молча:
Deprecation/Sunset-заголовки, срок жизни, заметки по миграции.
Идемпотентность
- GET/PUT/DELETE идемпотентны по контракту — клиент может безопасно ретраить.
- POST с побочным эффектом (платёж, заказ) — Idempotency-Key: клиент шлёт уникальный ключ заголовком; сервер хранит
key → результат(Redis/БД, TTL ~24ч) и на повтор возвращает сохранённый ответ, не выполняя операцию дважды. Обязателен для денежных операций и любых ретраящихся вызовов (вебхуки, очереди). - Потребители очередей — идемпотентны всегда: at-least-once означает, что дубли будут.
Ошибки: RFC 7807 (application/problem+json)
Единый формат всех ошибок API:
{
"type": "https://api.example.com/errors/insufficient-funds",
"title": "Insufficient funds",
"status": 422,
"detail": "Balance 5.00 is less than order total 20.00",
"instance": "/orders/req-7f3a",
"code": "INSUFFICIENT_FUNDS",
"errors": [{ "field": "amount", "message": "…" }]
}
code— стабильный машинный код: по нему ветвится клиент; человекочитаемый текст может меняться и локализоваться.- Валидационные ошибки — списком по полям, все сразу, не по одной.
- Не течь внутренностями: stack trace, SQL, имена таблиц — только в логи; наружу — generic 500 +
instance/requestId для соотнесения с логами.
Пример хорошего эндпоинта
POST /v1/orders
Authorization: Bearer <token>
Idempotency-Key: 3f2a-…
{ "items": [{ "productId": "p_1", "qty": 2 }] }
201 Created
Location: /v1/orders/ord_9x1
{ "id": "ord_9x1", "status": "pending", "total": { "amount": 4200, "currency": "EUR" }, "createdAt": "2026-08-06T10:00:00Z" }
Деньги — минорными единицами + валюта (не float); даты — ISO 8601 в UTC; id — префиксованные строки. Ошибка того же эндпоинта — problem+json с 422 и машинным code.
Безопасность (минимум)
Аутентификация на каждом эндпоинте (Bearer/JWT), авторизация — на уровне ресурса (чужой orderId → 403/404), rate limiting per-user/per-key с 429, вход валидируется схемой (zod) до бизнес-логики.
Углублённо
- REST vs GraphQL vs gRPC: GraphQL — клиенты с разными потребностями в данных; gRPC — внутренняя сервис-сервис связь с жёсткими контрактами; дефолт публичного API — REST.
- Контракты и OpenAPI-покрытие → навык swagger-coverage.
- Коды и обработка ошибок → навык error-handling.
- Локализация ответов (i18n) → навык i18n: контракт ошибок — стабильный машинный
code+ параметры (не готовый переведённый текст), выбор языка поAccept-Language/локали пользователя.