Проектирование REST API
REST API - это договор между клиентом и сервером. Клиентом может быть frontend, мобильное приложение, другой backend-сервис или CLI. Сервер обещает: если вы отправите такой запрос, с такими данными и таким методом, вы получите такой ответ.
Хороший API предсказуем. Разработчик не должен угадывать, почему книги лежат в /books, пользователи в /getUsers, а удаление делается через /remove-book. Чем меньше сюрпризов, тем проще писать frontend, тесты и интеграции.
Ресурсы, а не действия
URL должен описывать сущность, а не команду:
GET /books GET /books/{id} POST /books PATCH /books/{id} DELETE /books/{id}
Плохо:
/getBooks /createBook /deleteBook
В REST действие уже есть в HTTP-методе. URL отвечает на вопрос "над чем работаем", метод отвечает на вопрос "что делаем".
Аналогия: /books/42 - это карточка книги в библиотеке. GET означает "покажи карточку", PATCH - "измени часть карточки", DELETE - "убери карточку".
HTTP-методы
Частые методы:
| Метод | Смысл |
|---|---|
GET | получить данные |
POST | создать ресурс или запустить операцию |
PUT | полностью заменить ресурс |
PATCH | частично обновить ресурс |
DELETE | удалить ресурс |
GET не должен менять данные. Это важно для кэшей, браузеров, прокси и повторных запросов.
POST обычно используют для создания:
POST /books
PATCH удобен, когда меняется часть ресурса:
{ "title": "New title" }
Статус-коды
Статус-код - короткий итог запроса.
| Код | Когда использовать |
|---|---|
200 OK | успешное чтение или обновление |
201 Created | ресурс создан |
202 Accepted | задача принята, но еще выполняется |
204 No Content | успешно, тела ответа нет |
400 Bad Request | некорректный ввод |
401 Unauthorized | пользователь не аутентифицирован |
403 Forbidden | доступа нет |
404 Not Found | ресурс не найден |
409 Conflict | конфликт состояния |
422 Unprocessable Entity | данные синтаксически верные, но не проходят правила |
429 Too Many Requests | rate limit |
500 Internal Server Error | ошибка сервера |
Не возвращайте 200 с текстом "error". Клиенты, прокси, мониторинг и тесты смотрят на статус-код.
Формат ошибок
Сделайте один формат ошибок для всего API:
{ "error": { "code": "validation_failed", "message": "Название книги обязательно", "fields": { "title": "required" } } }
message можно показать человеку. code удобно использовать в frontend-логике и тестах. fields помогает подсветить конкретные поля формы.
Не отдавайте пользователю внутренние ошибки базы данных:
pq: duplicate key value violates unique constraint ...
Лучше вернуть понятный ответ:
{ "error": { "code": "email_already_exists", "message": "Пользователь с таким email уже существует" } }
Пагинация
Списки должны иметь ограничение размера:
GET /books?limit=20
Для учебных проектов можно начать с page и limit:
GET /books?page=2&limit=20
Для больших данных лучше cursor pagination:
GET /books?limit=20&cursor=eyJpZCI6...
Ответ:
{ "items": [], "next_cursor": "eyJpZCI6..." }
Cursor pagination устойчивее, когда данные часто добавляются или удаляются.
Фильтры и сортировка
Фильтры обычно передают через query parameters:
GET /books?author_id=42&status=published&sort=-created_at
Хорошее правило: если параметр уточняет список, он идет в query string. Если параметр выбирает конкретный ресурс, он идет в path.
GET /books/42 GET /books?category=backend
Вложенные ресурсы
Если ресурс принадлежит другому, вложенный путь читается естественно:
GET /books/{book_id}/reviews POST /books/{book_id}/reviews
Но не делайте слишком глубокие цепочки:
/users/{user_id}/books/{book_id}/reviews/{review_id}/comments/{comment_id}
Чаще достаточно 1-2 уровней. Слишком глубокий URL обычно показывает, что границы ресурсов нужно пересмотреть.
Идемпотентность
Идемпотентная операция дает один и тот же итог при повторении. Например, DELETE /books/42 можно отправить два раза: после первого книга удалена, после второго она все еще удалена.
Это важно для retry. Если сеть оборвалась, клиент может не знать, дошел запрос или нет. Повторять безопаснее те операции, которые не создают дубликаты.
Для платежей, заказов и похожих операций часто используют idempotency key:
Idempotency-Key: 8a44a0f2-...
Сервер запоминает ключ и не выполняет одну и ту же операцию дважды.
CORS
CORS нужен браузеру, когда frontend и API находятся на разных origins. Например:
Frontend: http://localhost:3000 API: http://localhost:8080
Backend должен явно разрешить origin, методы и заголовки:
Access-Control-Allow-Origin: https://app.example.com Access-Control-Allow-Methods: GET, POST, PATCH, DELETE, OPTIONS Access-Control-Allow-Headers: Content-Type, Authorization
OpenAPI
OpenAPI-спецификация описывает эндпоинты, схемы запросов и ответов. Она полезна для:
- документации;
- генерации клиентов;
- ревью API до реализации;
- проверки контрактов;
- синхронизации backend и frontend.
Минимальный фрагмент:
paths: /books: get: summary: List books responses: "200": description: Books list
Даже если вы не генерируете код, OpenAPI заставляет заранее договориться о форме API.
Версионирование
Если API публичный или им пользуются несколько клиентов, изменения нужно делать осторожно. Удаление поля, изменение типа или переименование ключа может сломать клиента.
Популярный вариант:
/api/v1/books
Не нужно вводить версии в маленьком учебном проекте без необходимости, но нужно понимать, что API - это контракт. Контракт нельзя менять незаметно.
Чеклист
- URL описывает ресурс.
- HTTP-метод описывает действие.
GETне меняет состояние.- Статус-коды соответствуют результату.
- Ошибки имеют единый JSON-формат.
- Списки имеют пагинацию.
- Фильтры находятся в query parameters.
- Вложенность URL не уходит глубже 1-2 уровней без причины.
- Опасные операции защищены от случайных повторов.
- API задокументирован через OpenAPI или понятное README.