Как организовать версионирование REST API
Изменения, требующие обязательного обновления кода на стороне клиента:
🟢 Junior Level
Версионирование REST API — это набор практик и механизмов управления изменениями контракта интерфейса, позволяющий вносить изменения в структуру данных и бизнес-логику без нарушения работоспособности ранее выпущенных клиентов (веб-приложений, мобильных приложений, сторонних интеграций).
Что такое Breaking Changes (Ломающие изменения)?
Изменения, требующие обязательного обновления кода на стороне клиента:
- Удаление существующего поля из ответа JSON или его переименование (
userId->id). - Изменение типа данных поля (число
100стало строкой"100"или объект стал массивом). - Добавление нового обязательного поля в тело запроса
POST/PUT. - Изменение семантики или формата URL эндпоинта.
Неломающие изменения (Non-breaking changes): добавление нового необязательного поля в ответ или добавление нового эндпоинта. Клиенты, использующие паттерн Tolerant Reader (игнорирование неизвестных JSON-полей), продолжают работать без сбоев.
4 основные стратегии версионирования
- Версионирование через URI (URI Path Versioning):
GET /api/v1/users/42 GET /api/v2/users/42Стандарт индустрии (GitHub, Stripe, Google, AWS). Самый простой и надежный подход.
- Версионирование через Query Parameters:
GET /api/users/42?version=2 - Версионирование через кастомный заголовок (Header Versioning):
GET /api/users/42 HTTP/1.1 X-API-Version: 2 - Версионирование через Content Negotiation (Media Type / Vendor MIME):
GET /api/users/42 HTTP/1.1 Accept: application/vnd.company.v2+json
🟡 Middle Level
Сравнительный анализ стратегий версионирования
| Критерий | URI Path (/v1/users) |
Custom Header (X-API-Version: 2) |
Content Negotiation (Accept: vnd.v2+json) |
|---|---|---|---|
| Кэширование на CDN/прокси | 🟢 Идеально (разные URL) | 🟡 Требует Vary: X-API-Version |
🟡 Требует Vary: Accept |
| Соответствие идеологии REST | 🔴 Нарушает (разные URI для одной сущности) | 🟡 Нейтрально | 🟢 Канонично по Рою Филдингу |
| Тестирование через браузер/Swagger | 🟢 Открывается прямой ссылкой | 🔴 Требует подстановки заголовков | 🔴 Требует специализированных клиентов |
| Сложность маршрутизации | 🟢 Тривиально (на уровне NGINX/Gateway) | 🟡 Требует анализа заголовков | 🔴 Парсинг MIME-типов |
| Популярность в продакшене | ~85% проектов | ~10% проектов | ~5% проектов |
Жизненный цикл устаревания API (RFC 8594 Sunset & Deprecation)
Для цивилизованного вывода старых версий API из эксплуатации сервер отправляет стандартизированные HTTP-заголовки:
HTTP/1.1 200 OK
Deprecation: @1735689600
Sunset: Wed, 31 Dec 2026 23:59:59 GMT
Link: <https://api.example.com/docs/v1-to-v2-migration>; rel="deprecation"; type="text/html"
Deprecation(IETF Draft): Сообщает, что версия признана устаревшей с указанной даты (UNIX timestamp).Sunset(RFC 8594): Точная дата и время в формате GMT, после наступления которых эндпоинт будет физически отключен.Link: Ссылка на руководство по миграции на новую версию.
Стратегия миграции БД без даунтайма: Expand and Contract (Parallel Run)
Если в v2 поле fullName разделяется на firstName и lastName, базу данных нельзя изменить мгновенно. Применяется 4-фазный паттерн:
[Фаза 1: Expand] ────> [Фаза 2: Dual Write] ────> [Фаза 3: Migrate & Read] ────> [Фаза 4: Contract]
Добавление новых Запись ведется и в v2 читает новые колонки, Удаление старой
колонок в БД старые, и в новые v1 постепенно выводится колонки fullName
(nullable) поля через триггер/код из эксплуатации из таблицы
🔴 Senior Level
Consumer-Driven Contract Testing (Pact / Spring Cloud Contract)
В микросервисной архитектуре версионирование каждого сервиса вручную приводит к комбинаторному взрыву зависимостей. Вместо ручного согласования версий применяется контрактное тестирование:
- Потребитель (Consumer, например Frontend или сервис заказов) пишет тест-контракт: «Мне от сервиса пользователей нужны поля
idиemail». - Сгенерированный контракт (JSON Pact) публикуется в Pact Broker.
- Поставщик (Provider, сервис пользователей) в пайплайне CI/CD проверяет каждый Pull Request против опубликованных контрактов всех потребителей.
- Если изменение удаляет поле
email, билд провайдера падает до попадания кода в production.
Реализация версионирования в Spring Boot 3
1. Версионирование через URI:
@RestController
@RequestMapping("/api/v1/users")
public class UserV1Controller {
@GetMapping("/{id}")
public UserDtoV1 getUser(@PathVariable Long id) { ... }
}
@RestController
@RequestMapping("/api/v2/users")
public class UserV2Controller {
@GetMapping("/{id}")
public UserDtoV2 getUser(@PathVariable Long id) { ... }
}
2. Версионирование через Content Negotiation (Media Type):
package com.example.controller;
import com.example.dto.UserDtoV1;
import com.example.dto.UserDtoV2;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/users")
public class UserVersioningController {
// Срабатывает при Accept: application/vnd.company.app-v1+json
@GetMapping(value = "/{id}", produces = "application/vnd.company.app-v1+json")
public ResponseEntity<UserDtoV1> getUserV1(@PathVariable Long id) {
return ResponseEntity.ok(new UserDtoV1(id, "Ivan Ivanov"));
}
// Срабатывает при Accept: application/vnd.company.app-v2+json
@GetMapping(value = "/{id}", produces = "application/vnd.company.app-v2+json")
public ResponseEntity<UserDtoV2> getUserV2(@PathVariable Long id) {
return ResponseEntity.ok(new UserDtoV2(id, "Ivan", "Ivanov", "ivan@example.com"));
}
}
Маршрутизация версий на уровне API Gateway (Canary Release)
Senior архитекторы выносят управление версиями на периметр системы — API Gateway (Spring Cloud Gateway / Envoy):
- Маршрутизация: Gateway преобразует внешний URL
/api/v1/usersво внутренний RPC-вызов или направляет на кластер подовuser-service-v1. - Канареечный релиз (Canary): 5% входящих запросов к
v1прозрачно направляются на экспериментальный инстансv2(Shadow Traffic) для проверки нагрузки и сравнения ответов без риска для пользователей. - Адаптер совместимости (BFF / Gateway Adapter): Если старую версию сервиса в коде уже удалили, Gateway может налету трансформировать ответ
v2в форматv1, поддерживая устаревшие мобильные клиенты без раздувания кодовой базы основного сервиса.
🎯 Шпаргалка для интервью
30-секундный ответ
Существует 4 основных способа версионирования REST API:
- URI Path (
/api/v1/users) — доминирующий стандарт индустрии (~85% сервисов). Максимально удобен для кэширования на CDN, маршрутизации на API Gateway и отладки разработчиками.- Query Parameters (
/users?v=1) — простой, но ухудшает читаемость URI.- Custom Headers (
X-API-Version: 1) — оставляет URI чистым, но требует обязательного заголовкаVaryдля кэширования.- Content Negotiation / Vendor MIME (
Accept: application/vnd.v1+json) — теоретически наиболее чистый по канонам REST, но сложен в отладке и интеграции.Устаревание версий стандартизировано заголовками
Sunset(RFC 8594) иDeprecation. Для миграции СУБД применяется паттерн Expand and Contract, а для контроля совместимости в микросервисах — Consumer-Driven Contract Testing (Pact).
4 каверзных вопроса с ответами
-
Почему версионирование через URI Path формально нарушает канонический REST Роя Филдинга, но всё равно является стандартом индустрии? Ответ: В теории REST URI идентифицирует сущность предметной области. Сущность «Пользователь 42» одна и та же, но адреса
/v1/users/42и/v2/users/42превращают ее в два разных ресурса. Однако прагматичные преимущества (прозрачное кэширование на CDN, простая маршрутизация на API Gateway и легкость тестирования в браузере) перевесили теоретическую строгость. -
Какая критическая проблема возникает при кэшировании ответов, если версия передается в заголовке
X-API-Version? Ответ: Если промежуточный кэш-сервер (CDN Cloudflare, NGINX) кэширует ответ только по URL, то первый клиент запросит версиюv1, а второй клиент с заголовкомv2получит из кэша устаревший ответv1. Для предотвращения сервер обязан передавать заголовокVary: X-API-Version. -
Что такое паттерн Expand and Contract и как он защищает базы данных при смене версий API? Ответ: Это метод изменения схемы БД без даунтайма: на этапе Expand в таблицу добавляются новые колонки рядом со старыми; на этапе параллельной работы приложение пишет в обе колонки; после перевода всех клиентов на новую версию API на этапе Contract старые колонки безопасно удаляются.
-
Что делать, если в мобильном приложении старая версия API зашита навсегда и пользователи не обновляют приложение? Ответ: Реализовать слой обратной совместимости (Compatibility Adapter) на уровне API Gateway или BFF (Backend For Frontend). Шлюз принимает запрос в формате старой версии
v1, трансформирует его в форматv2, вызывает актуальный микросервис и преобразует ответ обратно в структуруv1.
Красные флаги (чего категорически нельзя говорить)
- ❌ «Добавление нового необязательного поля в JSON-ответ — это ломающее изменение (Breaking Change)» (это неломающее изменение при соблюдении клиентами паттерна Tolerant Reader).
- ❌ «Версионирование в URL — это грубейшая ошибка, так никто в индустрии не делает» (так делают Stripe, GitHub, AWS, Google, Kubernetes).
- ❌ «При смене версии API можно сразу удалить старые колонки из БД и накатить миграцию с DROP COLUMN» (приведет к падению старых инстансов сервиса и немедленному даунтайму).
- ❌ «Header-based версионирование кэшируется на прокси точно так же, как URI» (без заголовка
Varyпрокси отдаст кэш одной версии клиенту другой версии).