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, а не имя таблицы
RepresentationJSON-представление ресурсаВерсионируйте контракт, не схему БД
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_..."
}

Типовые вопросы

  1. Почему POST /orders/{id}/cancel иногда допустим?
    • Отмена может быть доменной командой, не заменяющей представление заказа. Нужны явная семантика ретрая и конфликт для неподходящего статуса.
  2. Чем PUT отличается от PATCH?
    • PUT задаёт полное представление известного URI; PATCH описывает частичное изменение. В обоих случаях контракт должен различать отсутствующее поле и null.
  3. Когда вернуть 404, а когда 403?
    • 403 означает известный, но запрещённый доступ. 404 часто намеренно используют для ресурса, существование которого нельзя раскрывать; правило должно быть единым.
  4. Почему нельзя пагинировать только массивом без метаданных?
    • Клиент не знает, есть ли следующая страница и какой порядок принят. Верните cursor/next link либо явно документированный total и limit.
  5. Что делает идемпотентный ключ при timeout клиента?
    • Повтор с тем же ключом возвращает сохранённый результат первой попытки, а не создаёт второй заказ.
  6. Почему 200 для любой ошибки плох?
    • Прокси, SDK, мониторинг и клиент теряют HTTP-семантику; обработка ошибок становится завязанной на тело и легко пропускается.

Практика

  • Спроектируйте API каталога и оформления заказа.
    • Критерии приёмки: опубликованы OpenAPI-схемы list/get/create; list имеет стабильную сортировку и ограниченный limit; create возвращает 201 и Location; ошибки имеют единый schema.
  • Добавьте защиту от повторной оплаты.
    • Критерии приёмки: два параллельных запроса с одинаковым 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 — описание и тестирование контрактов.