Транспорт бэкенда
REST API: архитектура и best practices
Рой Филдинг написал диссертацию в 2000 году, описав REST как архитектурный стиль для масштабируемых распределённых систем. Большинство API, называемых REST, нарушают минимум три из шести его ограничений - и при этом вполне успешно работают. Вопрос в том, какие нарушения критичны, а какие - разумные компромиссы.
- **Stripe API** - эталон REST дизайна: ресурсное именование, Idempotency-Key для платежей, версионирование через URI (/v1/), детальные статус-коды
- **GitHub REST API** - HATEOAS-like `_links` в ответах, pagination через Link header, версионирование через Accept header
- **Twitter/X API v2** - переход от RPC-style v1 (`/statuses/show.json`) к ресурсному v2 (`/tweets/{id}`) как пример breaking migration
- **PayPal API** - один из немногих публичных API с настоящим HATEOAS, `_links` управляют флоу платежа
6 ограничений REST
В 2000 году Рой Филдинг защитил диссертацию, где описал REST как архитектурный стиль для распределённых систем. Большинство API, называющих себя REST, нарушают минимум три из шести его ограничений.
REST расшифровывается как **Representational State Transfer**. Это не протокол и не стандарт - набор архитектурных ограничений. HTTP - де-факто реализация, но REST теоретически применим к любому протоколу.
- **Uniform Interface** - единый интерфейс взаимодействия: ресурсы идентифицируются URI, манипуляции через представления, самоописательные сообщения
- **Stateless** - каждый запрос содержит всю информацию для его обработки; сервер не хранит состояние клиента между запросами
- **Cacheable** - ответы должны явно помечаться как кешируемые или нет (`Cache-Control`, `ETag`)
- **Client-Server** - разделение ответственности: UI и хранение данных разделены, можно развивать независимо
- **Layered System** - клиент не знает, общается ли с конечным сервером или прокси/балансировщиком
- **Code on Demand** (опционально) - сервер может присылать исполняемый код (JavaScript)
**Почему Stateless критичен для масштабирования:** если сервер хранит сессионное состояние в памяти, каждый следующий запрос клиента должен попасть на тот же инстанс. С Stateless любой инстанс обслуживает любой запрос - горизонтальное масштабирование без sticky sessions.
**REST != HTTP.** REST - архитектурный стиль. HTTP - протокол. REST описывает как строить системы, HTTP - как передавать данные. Можно нарушать REST поверх HTTP (Stateful API) или соблюдать REST поверх других протоколов.
Клиент получает JWT token при входе и отправляет его в каждом запросе. Какое ограничение REST это реализует?
Дизайн ресурсов и HTTP методы
Самая частая ошибка - именовать эндпоинты как действия (`/getUser`, `/createOrder`, `/deleteItem`). В REST URI идентифицирует **ресурс**, а не действие. Действие передаётся HTTP методом.
| Метод | Действие | Идемпотентен | Безопасен | Пример |
|---|---|---|---|---|
| GET | Получить ресурс | Да | Да | GET /users/123 |
| POST | Создать ресурс | Нет | Нет | POST /orders |
| PUT | Заменить ресурс целиком | Да | Нет | PUT /users/123 |
| PATCH | Обновить частично | Нет* | Нет | PATCH /orders/456 |
| DELETE | Удалить ресурс | Да | Нет | DELETE /items/789 |
Статус-коды HTTP - часть языка REST. Клиент должен понимать результат операции без парсинга тела ответа.
| Код | Значение | Когда использовать |
|---|---|---|
| 200 OK | Успех с телом | GET, PUT, PATCH - ресурс возвращается |
| 201 Created | Создан | POST - новый ресурс создан, Location header |
| 204 No Content | Успех без тела | DELETE, PUT когда тело не нужно |
| 400 Bad Request | Ошибка клиента | Невалидный JSON, отсутствуют обязательные поля |
| 401 Unauthorized | Нет аутентификации | Токен отсутствует или истёк |
| 403 Forbidden | Нет прав | Токен валиден, но нет доступа к ресурсу |
| 404 Not Found | Ресурс не найден | ID не существует |
| 409 Conflict | Конфликт состояния | Дублирующийся email, нарушение уникальности |
| 422 Unprocessable | Семантическая ошибка | JSON валиден, но данные некорректны |
| 500 Internal Error | Ошибка сервера | Непредвиденное исключение |
**Антипаттерн:** возвращать `200 OK` с `{"success": false, "error": "Not found"}`. Это заставляет клиента парсить тело для определения успеха - нарушает смысл HTTP статус-кодов.
Идемпотентность и безопасность
Сеть ненадёжна: запрос может уйти, но ответ потеряться. Клиент не знает - выполнилась ли операция. Решение: повторить запрос. Но что если сервер выполнит его дважды?
Два независимых свойства операций: **идемпотентность** (повторный вызов даёт тот же результат) и **безопасность** (нет побочных эффектов).
| Метод | Идемпотентен | Безопасен | Объяснение |
|---|---|---|---|
| GET | Да | Да | Чтение данных - повторный GET возвращает то же |
| HEAD | Да | Да | Как GET, только без тела |
| OPTIONS | Да | Да | Возвращает поддерживаемые методы |
| PUT | Да | Нет | PUT /users/1 с теми же данными - результат один |
| DELETE | Да | Нет | DELETE дважды: первый удаляет, второй - 404, состояние то же |
| POST | Нет | Нет | POST /orders создаёт новый заказ каждый раз |
| PATCH | Нет* | Нет | PATCH SET balance=100 - идемпотентен; PATCH balance+=10 - нет |
**Stripe, Braintree, Adyen** - все платёжные системы реализуют Idempotency-Key. Клиент генерирует UUID перед запросом, передаёт в заголовке. Серверы хранят ответы 24-72 часа. Это стандартный паттерн для финансовых операций.
DELETE /users/42 вызван дважды. Первый вызов удалил пользователя, второй вернул 404. DELETE идемпотентен?
Версионирование API
API изменяется - добавляются поля, меняются структуры, удаляются эндпоинты. Если клиенты (мобильные приложения, партнёры) не могут обновиться мгновенно, нужна стратегия сосуществования версий.
Ключевое разделение: **breaking changes** требуют новой версии, **non-breaking** - нет.
| Breaking change | Non-breaking change |
|---|---|
| Удаление поля из ответа | Добавление нового опционального поля |
| Переименование поля | Добавление нового эндпоинта |
| Изменение типа поля (string → number) | Добавление новых HTTP методов |
| Изменение семантики существующего поля | Расширение enum новыми значениями (с осторожностью) |
| Изменение структуры URL | Добавление необязательных query параметров |
**Стратегия sunset:** при выпуске v2 не удалять v1 немедленно. Добавить заголовок `Sunset: Sat, 01 Jan 2026 00:00:00 GMT` в ответы v1 - клиенты могут логировать его и планировать миграцию.
В API v1 поле `user.name` возвращает строку. В новой версии нужно разделить на `firstName` и `lastName`. Это breaking change?
HATEOAS: гиперсвязи в ответах
HATEOAS - Hypermedia As The Engine Of Application State - последнее и самое редко реализуемое ограничение REST. Идея: ответ сервера содержит не только данные, но и ссылки на допустимые следующие действия.
На практике подавляющее большинство «REST API» - это RPC-over-HTTP: глаголы в URL или жёстко зашитые маршруты без гиперссылок. Fielding публично критиковал такие API, называя их «не REST».
**Когда HATEOAS реально нужен:** публичные API с длинным жизненным циклом (PayPal API использует HATEOAS), где клиенты - сторонние разработчики, которые не должны жёстко зашивать URL. Для внутренних API микросервисов HATEOAS обычно избыточен.
Любой API поверх HTTP с JSON - это REST API
REST - набор из 6 архитектурных ограничений. Большинство HTTP API нарушают хотя бы Stateless или Uniform Interface
Термин REST стал маркетинговым синонимом 'не SOAP'. Fielding в 2008 написал пост 'REST APIs must be hypertext-driven', где критикует подмену понятий
API возвращает заказ со ссылками `_links.pay` и `_links.cancel`. При статусе 'shipped' эти ссылки отсутствуют. Что это обеспечивает?
REST API: ключевые идеи
- REST - 6 архитектурных ограничений Fielding 2000; Stateless критичен для горизонтального масштабирования
- URI идентифицирует ресурс (существительное), HTTP метод - действие; статус-коды несут семантику без парсинга тела
- Идемпотентность (GET/PUT/DELETE) позволяет безопасно ретраить запросы; Idempotency-Key решает проблему POST
- HATEOAS - редко реализуемый, но концептуально важный принцип: сервер управляет допустимыми переходами состояния
Связанные темы
REST - синхронный протокол взаимодействия, существующий в экосистеме других подходов к API дизайну:
- GraphQL — Альтернатива REST: один эндпоинт, клиент задаёт структуру ответа
- gRPC — Бинарный протокол поверх HTTP/2, альтернатива для микросервисов
- HTTP Fundamentals — Транспортный уровень REST: методы, заголовки, статус-коды
Вопросы для размышления
- Чем отличается идемпотентность от безопасности HTTP методов, и почему DELETE идемпотентен, хотя изменяет состояние?
- При каких условиях HATEOAS добавляет реальную ценность, а при каких становится избыточным усложнением?
- Как stateless ограничение REST влияет на выбор механизма аутентификации (сессии vs JWT)?