Принципы REST: HTTP-методы, статус-коды, идемпотентность
REST, это не библиотека и не протокол. Это архитектурный стиль с шестью принципами. Из них три самых важных для повседневной работы: единообразный интерфейс, клиент-серверное разделение и stateless (без состояния). Именно они диктуют как правильно использовать HTTP.
HTTP-методы
Каждый HTTP-метод имеет чёткую семантику. Нарушать её, значит создавать API, который будет путать коллег и ломать кэши.
GET, получение данных
GET запрашивает ресурс. Он никогда не должен изменять данные на сервере.
GET /api/v1/products → список товаров
GET /api/v1/products/42 → товар с id=42
GET /api/v1/users/5/orders → заказы пользователя 5
GET-запросы кэшируются браузером и CDN. Если твой GET меняет данные, кэш сломает логику.
POST, создание ресурса
POST создаёт новый ресурс. URL не содержит ID, он присваивается сервером.
POST /api/v1/products
Content-Type: application/json
{
"name": "Ноутбук Dell XPS",
"price": 89999,
"category_id": 3
}
Ответ, обычно 201 Created с заголовком Location или телом с созданным ресурсом:
HTTP/1.1 201 Created
Location: /api/v1/products/143
{
"id": 143,
"name": "Ноутбук Dell XPS",
"price": 89999,
"created_at": "2024-03-15T10:23:00Z"
}
PUT, полная замена ресурса
PUT полностью заменяет ресурс. Если поле не указано в теле, оно будет сброшено или стать null.
PUT /api/v1/products/143
Content-Type: application/json
{
"name": "Ноутбук Dell XPS 15",
"price": 94999,
"category_id": 3
}
Правило: клиент должен отправить все поля ресурса, не только изменённые.
PATCH, частичное обновление
PATCH обновляет только те поля, которые указаны. Всё остальное остаётся как есть.
PATCH /api/v1/products/143
Content-Type: application/json
{
"price": 84999
}
Только цена изменится. Название, категория и прочие поля останутся прежними.
PUT vs PATCH на практике:
- Обновляешь весь объект целиком (форма редактирования) → PUT
- Меняешь одно поле (toggle статуса, изменение цены) → PATCH
DELETE, удаление
DELETE /api/v1/products/143
Ответ: 204 No Content (удалено, тела нет) или 200 OK с подтверждением.
Итоговая таблица методов
| Метод | Действие | Тело запроса | Идемпотентный | Безопасный |
|---|---|---|---|---|
| GET | Получить | Нет | Да | Да |
| POST | Создать | Да | Нет | Нет |
| PUT | Полностью заменить | Да | Да | Нет |
| PATCH | Частично обновить | Да | Условно | Нет |
| DELETE | Удалить | Нет/Да | Да | Нет |
Идемпотентность
Идемпотентность, это свойство операции, при котором повторное выполнение с теми же параметрами даёт тот же результат.
Математическая аналогия: умножение на 1 идемпотентно (5 × 1 = 5, и ещё раз 5 × 1 = 5). Прибавление 1, нет (5 + 1 = 6, ещё раз 6 + 1 = 7).
GET, идемпотентен. Сколько раз ни запроси /users/42, данные не меняются.
DELETE, идемпотентен. Удалить уже удалённый ресурс, то же самое состояние. Первый вызов вернёт 204, второй, 404, но состояние сервера одинаково: ресурса нет.
PUT, идемпотентен. Отправить одинаковое тело два раза, ресурс одинаков после обоих вызовов.
POST, не идемпотентен. Дважды отправить POST /orders создаст два заказа. Это критически важно при сетевых ошибках: если клиент не знает дошёл ли запрос, повтор POST может задублировать данные.
Почему это важно на практике
Клиент отправил POST /payments → сеть упала → ответ не получен
Клиент думает: "ошибка, нужно повторить"
Повторяет запрос → создаётся второй платёж
Решение для POST: idempotency key. Клиент генерирует уникальный ключ и передаёт в заголовке. Сервер запоминает результат по ключу и при повторе возвращает тот же ответ.
POST /api/v1/payments
Idempotency-Key: a8f3d2c1-4b5e-4c6d-8e7f-9a0b1c2d3e4f
{ "amount": 5000, "currency": "RUB" }
Именно так работают Stripe и другие платёжные системы.
HTTP статус-коды
Статус-код, это трёхзначное число, которое говорит клиенту что произошло. Правильные коды делают API самодокументируемым.
2xx, Успех
| Код | Название | Когда использовать |
|---|---|---|
| 200 | OK | GET, PUT, PATCH успешно выполнены |
| 201 | Created | POST создал новый ресурс |
| 204 | No Content | DELETE успешно, или PUT/PATCH без тела ответа |
| 202 | Accepted | Запрос принят, обрабатывается асинхронно |
3xx, Перенаправление
| Код | Название | Когда использовать |
|---|---|---|
| 301 | Moved Permanently | Ресурс навсегда переехал на новый URL |
| 302 | Found | Временный редирект |
| 304 | Not Modified | Кэш актуален, не пересылай данные |
4xx, Ошибка клиента
Клиент сделал что-то не так. Повторять запрос без изменений бессмысленно.
| Код | Название | Когда использовать |
|---|---|---|
| 400 | Bad Request | Неверный формат запроса, ошибки валидации |
| 401 | Unauthorized | Не аутентифицирован (нет токена или неверный) |
| 403 | Forbidden | Аутентифицирован, но нет прав на действие |
| 404 | Not Found | Ресурс не существует |
| 409 | Conflict | Конфликт состояния (например, дубликат email) |
| 422 | Unprocessable Entity | Синтаксис верен, но данные логически невалидны |
| 429 | Too Many Requests | Rate limit превышен |
5xx, Ошибка сервера
Клиент сделал всё правильно, но что-то сломалось на сервере. Повтор запроса может сработать.
| Код | Название | Когда использовать |
|---|---|---|
| 500 | Internal Server Error | Непредвиденная ошибка сервера |
| 502 | Bad Gateway | Upstream сервис вернул некорректный ответ |
| 503 | Service Unavailable | Сервис временно недоступен (деплой, перегрузка) |
| 504 | Gateway Timeout | Upstream сервис не ответил вовремя |
Частые ошибки с кодами
401 vs 403, распространённая путаница:
401 Unauthorized→ «Кто ты такой? Предъяви токен»403 Forbidden→ «Знаю кто ты, но тебе сюда нельзя»
400 vs 422:
400→ запрос синтаксически неверен (невалидный JSON)422→ JSON верный, но данные нарушают бизнес-правила
Не используй 200 для ошибок. Антипаттерн:
HTTP/1.1 200 OK
{ "success": false, "error": "User not found" }
Это ломает мониторинг, логирование и любую автоматизацию.
Принцип Stateless
REST требует, чтобы каждый запрос содержал всю необходимую информацию для его обработки. Сервер не хранит состояние между запросами.
# Правильно: токен в каждом запросе
GET /api/v1/profile
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...
# Неправильно: "ты же помнишь что я залогинился вчера"
GET /api/v1/profile
Stateless делает API горизонтально масштабируемым: любой из десяти серверов может обработать любой запрос, потому что вся информация уже в запросе.