REST, коды ответов и формат ошибок

Аналитик всё чаще проектирует контракты сам, поэтому REST спрашивают почти на каждом собеседовании — и почти всегда с подвохом про коды ответов.

REST — архитектурный стиль, где система описывается через ресурсы (существительные), над которыми выполняются стандартные HTTP-методы, а состояние клиента не хранится на сервере между запросами.

Вопрос 1. Что такое REST и какие у него принципы?

Что проверяют. Не путаете ли вы «REST» и «любой HTTP API с JSON». Половина API в мире называется REST, будучи RPC поверх HTTP, — и это нормально, но аналитик обязан видеть разницу.

Ключевые принципы, которые стоит назвать:

  • Ресурсная модель. URL адресует сущность, а не действие: /orders/1024, а не /getOrderById?id=1024. Глагол несёт HTTP-метод.
  • Единообразный интерфейс. Один и тот же набор методов работает одинаково для всех ресурсов.
  • Отсутствие состояния (stateless). Каждый запрос самодостаточен: сервер не помнит предыдущий. Отсюда — авторизация в каждом запросе и возможность горизонтального масштабирования.
  • Кэшируемость. Ответ на GET может быть закэширован, и API должен это явно обозначать.
  • Слои. Между клиентом и сервисом могут стоять шлюзы и балансировщики, клиент об этом не знает.
ПЛОХО (RPC в маске REST)      ХОРОШО (ресурсный стиль)
POST /createOrder             POST   /orders
GET  /getOrderList            GET    /orders?status=paid&page=2
POST /cancelOrderById         POST   /orders/1024/cancellation
GET  /getUserOrders?u=7       GET    /customers/7/orders

Отдельно оговорите действия, которые не ложатся на CRUD: отмена, подтверждение, пересчёт. Практика — оформлять их как подресурс-событие (POST /orders/1024/cancellation) либо как явный переход статуса. Это лучше, чем городить PATCH с волшебным полем action.

Вопрос 2. Какие коды ответов вы используете и когда?

Что проверяют. Знание не всей таблицы, а рабочего минимума и различий между парами, которые все путают: 401/403, 400/422, 404/410.

КодКогда
200 OKуспешное чтение или изменение с телом ответа
201 Createdсоздан новый ресурс; в заголовке Location — его адрес
202 Acceptedзапрос принят, обработка асинхронная; результат позже
204 No Contentуспех без тела (например, удаление)
400 Bad Requestзапрос синтаксически неверен: сломан JSON, не тот тип поля
401 Unauthorizedне понятно, кто вы: токена нет или он невалиден
403 Forbiddenпонятно, кто вы, но вам нельзя
404 Not Foundресурса нет (или мы не хотим раскрывать, что он есть)
409 Conflictконфликт состояния: заказ уже отменён, версия устарела
422 Unprocessable Entityсинтаксис верен, но нарушено бизнес-правило
429 Too Many Requestsпревышен лимит; ответ с Retry-After
500 / 503наша ошибка / сервис временно недоступен

Формулировка, которая звучит зрело: «4xx — клиент виноват, чинить на его стороне; 5xx — мы виноваты, клиенту имеет смысл повторить позже». Именно поэтому нельзя отдавать 200 с телом {"error": "..."}: клиентские библиотеки, ретраи и мониторинг ориентируются на код, и такой API невозможно нормально эксплуатировать.

Вопрос 3. Как вы описываете формат ошибки?

Что проверяют. Думаете ли вы о том, кто и как будет эти ошибки обрабатывать. Хорошая ошибка машиночитаема и одновременно пригодна для показа пользователю.

{
  "type": "https://api.shop.ru/errors/validation",
  "title": "Ошибка валидации",
  "status": 422,
  "code": "ORDER_EMPTY",
  "detail": "Заказ должен содержать хотя бы одну позицию",
  "traceId": "b7c1f0a2-3d44-4a11-9f0e-77d1a2c9e5b3",
  "errors": [
    { "field": "items", "code": "MIN_ITEMS", "message": "минимум 1 позиция" },
    { "field": "items[0].qty", "code": "MIN_VALUE", "message": "должно быть больше 0" }
  ]
}

Что здесь важно и почему:

  • Стабильный code — по нему клиент ветвит логику. Текст detail можно менять и переводить, код — нельзя, это часть контракта.
  • Список errors с указанием поля — чтобы фронтенд подсветил конкретные поля формы, а не показал одно общее сообщение.
  • Все ошибки сразу, а не первая попавшаяся — иначе пользователь исправляет форму по одной ошибке за раз.
  • traceId — чтобы поддержка по обращению пользователя нашла запрос в логах.
  • Никаких внутренних деталей: стектрейсов, SQL-запросов, имён таблиц. Это и утечка, и лишний шум.

Структура выше близка к стандарту RFC 9457 (Problem Details) — упомянуть его на собеседовании полезно, но важнее объяснить смысл полей.

Типичные ошибки кандидатов

  • Называют глаголы в URL нормой: «главное, чтобы работало». Работать будет, но это не REST, и на вопрос надо отвечать честно: «это RPC-стиль, у него свои плюсы».
  • Путают 401 и 403. Мнемоника: 401 — «я тебя не узнаю», 403 — «я тебя узнал, тебе нельзя».
  • Отдают 200 на ошибку. Ломается всё: ретраи, алерты, метрики.
  • Возвращают 404 на пустой список. Пустой список — это 200 и []; 404 означает, что нет самого ресурса-коллекции.
  • Не описывают пагинацию и сортировку у списочных методов, и её потом «додумывает» разработчик.
  • Забывают, что stateless означает передачу авторизации в каждом запросе, и проектируют «сессию» на стороне API.

Как ответить кратко

REST — это ресурсный стиль: URL адресует сущность существительным, действие несёт HTTP-метод, каждый запрос самодостаточен, ответы кэшируемы. Действия вне CRUD оформляю подресурсом: POST /orders/1024/cancellation. Коды: 201 при создании с заголовком Location, 202 для асинхронной обработки, 400 — сломан синтаксис, 422 — нарушено бизнес-правило, 401 — не узнали, 403 — узнали и запрещаем, 409 — конфликт состояния, 429 с Retry-After при лимитах. 4xx — вина клиента, 5xx — наша; отдавать 200 с ошибкой в теле нельзя, это ломает ретраи и мониторинг. Ошибку описываю машиночитаемо: стабильный код, список ошибок по полям и traceId для поддержки.

Проверьте себя
1. Запрос синтаксически корректен, но заказ нельзя создать без единой позиции. Какой код ответа уместнее?
A400 Bad Request
B422 Unprocessable Entity
C409 Conflict
D500 Internal Server Error
2. В чём разница между 401 и 403?
A401 — временная ошибка, 403 — постоянная
B401 — «не узнаю, кто вы» (нет или невалиден токен), 403 — «узнал, но вам нельзя»
C401 отдаёт шлюз, 403 — само приложение
D401 используется для API, 403 — для веб-страниц
3. Почему нельзя возвращать 200 OK с телом вида {"error": "..."}?
AЭто запрещено спецификацией JSON
BКлиентские ретраи, мониторинг и метрики ориентируются на код ответа, и такой API невозможно нормально эксплуатировать
CТело ответа при 200 обязано быть пустым
DТак ошибка не попадёт в лог сервера