Проектирование REST API: именование, версии, ошибки
Практика3 минРедакция Innovialabs
Коротко
Хороший REST API предсказуем: существительные во множественном числе в URL, единые правила пагинации и фильтров, явная версия, один формат ошибок для всех эндпоинтов и ключи идемпотентности для операций, которые нельзя повторять.
Главный принцип: предсказуемость
Хороший API понятен без чтения документации к каждому эндпоинту. Разработчик увидел два адреса — и уже догадывается, как устроен третий. Ниже правила, которые дают этот эффект.
Именование URL
Правила:
- Существительные во множественном числе:
/products,/orders. - Без глаголов: действие задаёт HTTP-метод.
- kebab-case и нижний регистр:
/delivery-zones, а не/DeliveryZones. - Вложенность не глубже одного уровня:
/orders/15/items— нормально,/users/3/orders/15/items/2/reviews— уже тяжело.
| Плохо | Хорошо |
|---|---|
GET /getAllProducts | GET /products |
POST /createOrder | POST /orders |
POST /deleteUser?id=3 | DELETE /users/3 |
GET /Product_List | GET /products |
Для действий, которые не укладываются в CRUD, допустим подресурс: POST /orders/15/cancel. Главное — один стиль на весь API.
Пагинация
Никогда не отдавайте коллекцию целиком: данные растут, и однажды ответ станет огромным.
Два распространённых подхода:
- Offset:
GET /products?limit=20&offset=40. Просто, позволяет прыгать на нужную страницу, но медленнеет на больших таблицах и может «терять» записи при вставках. - Cursor:
GET /products?limit=20&cursor=eyJpZCI6NDJ9. Стабилен для лент и больших объёмов, но нельзя перейти сразу на страницу 10.
Ответ должен содержать данные и информацию для следующего шага:
{
"data": [{ "id": 41, "name": "Чайник" }],
"pagination": { "nextCursor": "eyJpZCI6NDJ9", "hasMore": true }
}
Задайте максимальный limit на сервере, чтобы клиент не запросил миллион записей.
Фильтрация и сортировка
Используйте query-параметры с понятными именами:
GET /products?category=kitchen&minPrice=100000&sort=-createdAt
- Фильтры — обычные параметры с именами полей.
- Сортировка — один параметр
sort, минус означает убывание. - Поиск — отдельный параметр, например
q. - Неизвестный параметр лучше отклонить с 400, чем молча проигнорировать.
Версионирование
API, которым пользуются внешние клиенты, рано или поздно изменится несовместимо. Версия позволяет не ломать старых клиентов.
- В URL:
/v1/products— самый наглядный и простой вариант. - В заголовке:
Accept: application/vnd.example.v2+json— чище URL, но сложнее отлаживать.
Что считать ломающим изменением: удаление или переименование поля, смена типа, новое обязательное поле в запросе. Добавление нового необязательного поля в ответ версию обычно не требует. Старую версию объявляйте устаревшей заранее и давайте клиентам время на переход.
Единый формат ошибок
Все эндпоинты должны возвращать ошибки одинаково. Удобная основа — стандарт Problem Details (RFC 9457):
{
"type": "https://api.example.com/errors/validation",
"title": "Validation failed",
"status": 422,
"detail": "Поле price должно быть больше нуля",
"errors": [{ "field": "price", "message": "must be > 0" }]
}
Правила:
- Правильный HTTP-код всегда, а не 200 с ошибкой внутри.
- Машиночитаемый тип или код ошибки, чтобы клиент мог на него реагировать.
- Понятное сообщение для разработчика.
- Никаких stack trace и SQL-запросов в ответе.
Ключи идемпотентности
Для операций, повтор которых опасен — оплата, создание заказа, — клиент передаёт уникальный ключ:
POST /payments
Idempotency-Key: 7f3c2a90-1b4e-4d2a-9c11-5e8f0a6b7d21
Сервер сохраняет результат по этому ключу. Если запрос пришёл повторно — из-за таймаута или двойного клика — сервер возвращает уже сохранённый ответ, а не создаёт второй платёж.
Частые ошибки
- Разные стили в одном API:
camelCaseв одних ответах,snake_caseв других. - Даты в локальном формате. Используйте ISO 8601 с часовым поясом.
- Отдача внутренних id и полей базы как есть. API — это контракт, а не дамп таблицы.
- Нет документации. Спецификация OpenAPI решает это и позволяет генерировать клиентов.