Как проектировать идемпотентные и версионные REST API
Вы развертываете новую функцию, и вдруг ваш API получает дублирующиеся POST-запросы от клиентов, повторяющих попытки после тайм-аута. Теперь в вашей базе данных два одинаковых заказа. Или вы выпускаете ломающее изменение, и мобильные приложения падают, потому что ожидали старый формат ответа. Это две из самых распространенных и болезненных проблем в проектировании API: неидемпотентные операции и ломающие изменения. Это руководство покажет, как решить обе проблемы с помощью практических паттернов идемпотентности и версионирования в REST API.
Почему идемпотентность важна в REST API
Идемпотентность означает, что выполнение одного и того же запроса несколько раз дает тот же эффект, что и однократное выполнение. HTTP-методы имеют определенную семантику идемпотентности: GET, HEAD, PUT, DELETE и OPTIONS идемпотентны, а POST и PATCH — нет. Но реальные API часто должны принимать POST для таких операций, как создание ресурсов или обработка платежей. Сетевые сбои, тайм-ауты клиентов и логика повторных попыток могут вызывать дубликаты. Без идемпотентности вы рискуете двойными списаниями, дублирующимися записями или несогласованным состоянием.
Методы создания идемпотентных REST API
1. Используйте ключи идемпотентности для POST-запросов
Наиболее распространенный подход — позволить клиентам отправлять уникальный ключ (например, UUID) в заголовке Idempotency-Key. Сервер сохраняет ключ и результат операции. Если тот же ключ получен снова, сервер возвращает сохраненный результат вместо повторного выполнения операции.
POST /payments
Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000
Content-Type: application/json
{
"amount": 1000,
"currency": "USD"
}
Шаги реализации:
- Клиент генерирует уникальный ключ (UUID v4) для каждой логической операции.
- Сервер проверяет, существует ли ключ в постоянном хранилище (например, Redis или базе данных).
- Если нет, обрабатывает запрос и сохраняет ключ вместе со статусом и телом ответа.
- Если да, возвращает сохраненный ответ без повторной обработки.
Установите разумный срок истечения (например, 24 часа), чтобы избежать неограниченного роста хранилища.
2. Используйте условные запросы
Для обновлений используйте заголовки ETag и If-Match, чтобы предотвратить потерянные обновления. Сервер возвращает ETag, представляющий текущее состояние. Клиент отправляет его обратно с If-Match; если ресурс изменился, сервер возвращает 412 Precondition Failed. Это делает PUT-запросы безопасными против состояний гонки.
PUT /articles/123
If-Match: "abc123"
Content-Type: application/json
{"title": "Updated title"}
3. Проектируйте PUT для естественной идемпотентности
Предпочитайте PUT вместо POST, когда клиент может определить URL ресурса. Например, PUT /users/{userId} естественно идемпотентен, потому что повторные вызовы перезаписывают тот же ресурс. Используйте POST только тогда, когда ID назначает сервер.
4. Обрабатывайте дубликаты с помощью уникальных ограничений
Комбинируйте ключи идемпотентности с уникальными ограничениями базы данных на бизнес-ключи (например, номер заказа). Если дубликат все же прошел, база данных отклонит его, и вы можете вернуть 409 Conflict с понятным сообщением.
Стратегии версионирования REST API
API развиваются. Новые поля, измененное поведение или удаленные эндпоинты могут нарушить работу существующих клиентов. Версионирование позволяет вводить изменения, не нарушая их. Существует несколько распространенных стратегий:
| Стратегия | Пример | Плюсы | Минусы |
|---|---|---|---|
| URI-путь | /v1/users |
Явный, простой в маршрутизации, удобный для кэширования | URL меняются, может загромождать |
| Параметр запроса | /users?version=1 |
Простой, необязательный | Легко пропустить, не соответствует REST |
| Пользовательский заголовок | Accept-Version: v1 |
Сохраняет URL чистыми | Сложнее тестировать в браузере |
| Media Type | Accept: application/vnd.api.v1+json |
Согласование контента, чистый REST | Сложный, менее распространенный |
Для большинства команд версионирование через URI-путь — наиболее прагматичный выбор. Оно видимо, легко документируется и хорошо работает с API-шлюзами и CDN.
Как версионировать, не нарушая работу клиентов
- Начните с v1 в пути с первого дня. Добавить версионирование позже сложнее.
- Только аддитивные изменения в рамках версии: новые необязательные поля, новые эндпоинты. Никогда не удаляйте и не переименовывайте поля.
- Устаревание с достоинством: объявите о прекращении поддержки, предоставьте руководства по миграции и отслеживайте использование старых версий.
- Используйте заголовки Sunset:
Sunset: Sat, 31 Dec 2025 23:59:59 GMTдля информирования клиентов. - Поддерживайте не более двух активных версий, чтобы ограничить нагрузку на поддержку.
Собираем все вместе: практический пример
Рассмотрим API платежей. Вы хотите идемпотентно создать платеж и версионировать эндпоинт.
POST /v1/payments
Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000
Content-Type: application/json
{
"amount": 1000,
"currency": "USD"
}
Логика сервера:
- Проверить, существует ли
Idempotency-Keyв Redis. - Если да, вернуть кэшированный ответ (статус + тело).
- Если нет, обработать платеж, сохранить ключ с ответом, вернуть 201 Created.
Для обновлений используйте PUT с ETag:
PUT /v1/payments/123
If-Match: "xyz789"
Content-Type: application/json
{"amount": 1500}
Если ETag не совпадает, верните 412. Это предотвращает перезапись параллельных изменений.
Распространенные ошибки и как их избежать
- Хранение ключей идемпотентности навсегда: установите TTL (например, 24 ч), чтобы избежать разбухания хранилища.
- Игнорирование состояний гонки: используйте атомарные операции (например, Redis SETNX) для проверки и установки ключей.
- Слишком позднее версионирование: добавьте /v1 с самого начала.
- Ломающие изменения в минорных версиях: считайте любое изменение, меняющее структуру ответа, мажорным.
- Отсутствие документации по идемпотентности: четко указывайте, какие эндпоинты поддерживают Idempotency-Key.
FAQ
Что такое идемпотентный REST API?
Идемпотентный REST API гарантирует, что выполнение одного и того же запроса несколько раз дает тот же результат, что и однократное выполнение. Это критически важно для безопасной обработки повторных попыток, особенно для неидемпотентных методов, таких как POST.
Какие HTTP-методы идемпотентны?
GET, HEAD, PUT, DELETE, OPTIONS и TRACE идемпотентны. POST и PATCH не идемпотентны по умолчанию, но вы можете сделать их идемпотентными с помощью таких методов, как ключи идемпотентности.
Какой лучший способ версионирования REST API?
Версионирование через URI-путь (например, /v1/users) — наиболее распространенный и практичный подход. Оно явное, легко маршрутизируется и хорошо работает с кэшированием и API-шлюзами. Другие варианты включают параметры запроса, пользовательские заголовки и media types, но у них есть компромиссы.
Готовы протестировать JSON-ответы вашего API? Используйте наш JSON Formatter для быстрой проверки и форматирования полезной нагрузки.