REST: дизайн HTTP API
REST — архитектурный стиль для распределённых систем, а не список URL с существительными. Его полезная часть для прикладного API — единый интерфейс HTTP: ресурс имеет стабильный идентификатор, представление передаётся через медиа-тип, а смысл операции задают метод, статус, заголовки и тело. Клиенту не нужно знать внутреннюю БД или последовательность RPC-вызовов.
Зачем это на интервью
На интервью проверяют умение превратить продуктовый сценарий в контракт, который переживёт ретраи, параллельные запросы и изменение реализации. Хороший ответ связывает POST /orders, GET /orders/{id}, коды ответов, валидацию, пагинацию, права доступа и последствия повторной доставки запроса.
Минимум для E4
- Моделировать в URL ресурсы и коллекции:
/v1/orders,/v1/orders/{orderId}, а не глаголы вроде/createOrder. - Выбирать HTTP-метод по семантике:
GETбезопасен,PUTзаменяет известное представление,PATCHчастично меняет ресурс,POSTсоздаёт подресурс или запускает неидемпотентную обработку. - Возвращать точный статус:
201 CreatedсLocation,204 No Contentбез тела,400для синтаксически неверного запроса,401без аутентификации,403без права,404для невидимого/отсутствующего ресурса,409для конфликта состояния,422для содержательно невалидных данных. - Делать единый формат ошибок с машиночитаемым
code, понятнымmessage, ссылкой/полем ошибки и корреляционным идентификатором. - Определять фильтры, сортировку и пагинацию явно; ограничивать
limitна сервере и не выдавать неограниченную коллекцию. - Документировать обязательность,
null, отсутствие поля, формат дат, денежные единицы и идемпотентность каждой мутации.
Углубление для E5/Senior
Ресурс — не обязательно строка таблицы. Заказ, экспорт и операция оплаты могут быть отдельными ресурсами с собственным жизненным циклом. Долгую работу лучше принять через POST /exports, вернуть 202 Accepted и URI операции; клиент затем читает состояние, а не удерживает HTTP-соединение до завершения.
Идемпотентность метода не гарантирует идемпотентность реализации. PUT /profiles/42 должен дать один итог при одинаковом теле, но отправка письма внутри обработчика может повториться при ретрае. Для создания с внешним эффектом принимайте Idempotency-Key, сохраняйте ключ вместе с хешем релевантного запроса и воспроизводите исходный результат. Один ключ с другим payload — конфликт, а не новая операция. Хранилище ключей имеет TTL, область действия пользователя/операции и защиту от конкурентных дублей.
Конкурентное редактирование требует precondition. Сервер выдаёт ETag, клиент отправляет If-Match; несовпадение возвращает 412 Precondition Failed. Это лучше «last write wins», когда потеря изменения недопустима. 409 Conflict описывает конфликт бизнес-состояния, например отмену уже отгруженного заказа; 412 — несоответствие заявленному клиентом представлению.
Не раскрывайте модели хранения. Внешний DTO — контракт: добавление нового поля обычно совместимо, переименование/изменение типа — нет. Поля прав доступа фильтруйте на сервере, а не скрывайте в UI. Курсорная пагинация устойчивее offset при частых вставках: cursor должен быть непрозрачным, включать порядок и tie-breaker, а сортировка — стабильной. При этом курсор не даёт снимок данных сам по себе; для строгой консистентности нужны snapshot/version или ограниченная выдача.
Ключевые понятия
| Понятие | Смысл | Практика |
|---|---|---|
| Resource | Адресуемая предметная сущность или процесс | Используйте устойчивый URI, а не имя таблицы |
| Representation | JSON-представление ресурса | Версионируйте контракт, не схему БД |
| Safe method | Не меняет наблюдаемое состояние | GET, HEAD, OPTIONS можно повторять |
| Idempotent method | Повтор одинакового запроса даёт одинаковый предполагаемый эффект над состоянием | PUT/DELETE требуют такой реализации; ответы и incidental effects могут отличаться |
| ETag | Версия представления | Защищает обновление через If-Match |
| Cursor | Непрозрачная позиция списка | Используется вместе со стабильной сортировкой |
Пример контракта
POST /v1/orders HTTP/1.1
Idempotency-Key: 2d2e6e0d-7c0c-4a8e-aeb4-3a3d6f50d8fa
Content-Type: application/json
{"items":[{"sku":"book-go","quantity":2}]}
HTTP/1.1 201 Created
Location: /v1/orders/ord_01J...
Content-Type: application/json
{"id":"ord_01J...","status":"pending","items":[{"sku":"book-go","quantity":2}]}Ошибку делайте стабильной для кода клиента, но не выдавайте stack trace и внутренние имена таблиц:
{
"code": "quantity_out_of_range",
"message": "quantity must be between 1 and 100",
"field": "items[0].quantity",
"requestId": "req_..."
}Типовые вопросы
- Почему
POST /orders/{id}/cancelиногда допустим?- Отмена может быть доменной командой, не заменяющей представление заказа. Нужны явная семантика ретрая и конфликт для неподходящего статуса.
- Чем
PUTотличается отPATCH?PUTзадаёт полное представление известного URI;PATCHописывает частичное изменение. В обоих случаях контракт должен различать отсутствующее поле иnull.
- Когда вернуть
404, а когда403?403означает известный, но запрещённый доступ.404часто намеренно используют для ресурса, существование которого нельзя раскрывать; правило должно быть единым.
- Почему нельзя пагинировать только массивом без метаданных?
- Клиент не знает, есть ли следующая страница и какой порядок принят. Верните cursor/next link либо явно документированный total и limit.
- Что делает идемпотентный ключ при timeout клиента?
- Повтор с тем же ключом возвращает сохранённый результат первой попытки, а не создаёт второй заказ.
- Почему
200для любой ошибки плох?- Прокси, SDK, мониторинг и клиент теряют HTTP-семантику; обработка ошибок становится завязанной на тело и легко пропускается.
Практика
- Спроектируйте API каталога и оформления заказа.
- Критерии приёмки: опубликованы OpenAPI-схемы list/get/create; list имеет стабильную сортировку и ограниченный
limit; create возвращает201иLocation; ошибки имеют единый schema.
- Критерии приёмки: опубликованы OpenAPI-схемы list/get/create; list имеет стабильную сортировку и ограниченный
- Добавьте защиту от повторной оплаты.
- Критерии приёмки: два параллельных запроса с одинаковым
Idempotency-Keyсоздают ровно одну операцию; повтор после timeout возвращает исходный response; иной payload с ключом получает документированный конфликт.
- Критерии приёмки: два параллельных запроса с одинаковым
- Реализуйте optimistic locking редактирования профиля.
- Критерии приёмки:
GETвыдаёт ETag; устаревшийIf-Matchполучает412; успешное обновление меняет ETag; интеграционный тест проверяет параллельных клиентов.
- Критерии приёмки:
Частые ошибки и ловушки
- Называть REST-ом API только из-за JSON и URL с существительными, игнорируя статусы, кеширование и единый контракт.
- Возвращать
200для create и ошибки без различимого формата. - Смешивать
null, отсутствующее поле и пустую строку приPATCH. - Делать offset-пагинацию по нестабильному
created_atбез уникального tie-breaker. - Ретраить неидемпотентную мутацию в клиенте без ключа и понимания исхода.
- Отдавать в ответе поля, которые UI «просто не показывает», но пользователь не должен видеть.
Связанные темы
База разработки и Computer Science · RPC и gRPC · Версионирование и совместимость API · Cookies, сессии, CORS и CSRF
Источники
- RFC 9110 — HTTP Semantics: методы, статусы, conditional requests.
- RFC 9457 — Problem Details for HTTP APIs.
- RFC 7232 — validators и
ETag/If-Match. - Fielding, Architectural Styles and the Design of Network-based Software Architectures — ограничения REST.
- OpenAPI Specification 3.1 — описание и тестирование контрактов.