🌐 Раздел 6 · Вопрос #15

Как организовать версионирование REST API

Изменения, требующие обязательного обновления кода на стороне клиента:


🟢 Junior Level

Версионирование REST API — это набор практик и механизмов управления изменениями контракта интерфейса, позволяющий вносить изменения в структуру данных и бизнес-логику без нарушения работоспособности ранее выпущенных клиентов (веб-приложений, мобильных приложений, сторонних интеграций).

Что такое Breaking Changes (Ломающие изменения)?

Изменения, требующие обязательного обновления кода на стороне клиента:

  • Удаление существующего поля из ответа JSON или его переименование (userId -> id).
  • Изменение типа данных поля (число 100 стало строкой "100" или объект стал массивом).
  • Добавление нового обязательного поля в тело запроса POST/PUT.
  • Изменение семантики или формата URL эндпоинта.

Неломающие изменения (Non-breaking changes): добавление нового необязательного поля в ответ или добавление нового эндпоинта. Клиенты, использующие паттерн Tolerant Reader (игнорирование неизвестных JSON-полей), продолжают работать без сбоев.

4 основные стратегии версионирования

  1. Версионирование через URI (URI Path Versioning):
    GET /api/v1/users/42
    GET /api/v2/users/42
    

    Стандарт индустрии (GitHub, Stripe, Google, AWS). Самый простой и надежный подход.

  2. Версионирование через Query Parameters:
    GET /api/users/42?version=2
    
  3. Версионирование через кастомный заголовок (Header Versioning):
    GET /api/users/42 HTTP/1.1
    X-API-Version: 2
    
  4. Версионирование через 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)

В микросервисной архитектуре версионирование каждого сервиса вручную приводит к комбинаторному взрыву зависимостей. Вместо ручного согласования версий применяется контрактное тестирование:

  1. Потребитель (Consumer, например Frontend или сервис заказов) пишет тест-контракт: «Мне от сервиса пользователей нужны поля id и email».
  2. Сгенерированный контракт (JSON Pact) публикуется в Pact Broker.
  3. Поставщик (Provider, сервис пользователей) в пайплайне CI/CD проверяет каждый Pull Request против опубликованных контрактов всех потребителей.
  4. Если изменение удаляет поле 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:

  1. URI Path (/api/v1/users) — доминирующий стандарт индустрии (~85% сервисов). Максимально удобен для кэширования на CDN, маршрутизации на API Gateway и отладки разработчиками.
  2. Query Parameters (/users?v=1) — простой, но ухудшает читаемость URI.
  3. Custom Headers (X-API-Version: 1) — оставляет URI чистым, но требует обязательного заголовка Vary для кэширования.
  4. Content Negotiation / Vendor MIME (Accept: application/vnd.v1+json) — теоретически наиболее чистый по канонам REST, но сложен в отладке и интеграции.

Устаревание версий стандартизировано заголовками Sunset (RFC 8594) и Deprecation. Для миграции СУБД применяется паттерн Expand and Contract, а для контроля совместимости в микросервисах — Consumer-Driven Contract Testing (Pact).

4 каверзных вопроса с ответами

  1. Почему версионирование через URI Path формально нарушает канонический REST Роя Филдинга, но всё равно является стандартом индустрии? Ответ: В теории REST URI идентифицирует сущность предметной области. Сущность «Пользователь 42» одна и та же, но адреса /v1/users/42 и /v2/users/42 превращают ее в два разных ресурса. Однако прагматичные преимущества (прозрачное кэширование на CDN, простая маршрутизация на API Gateway и легкость тестирования в браузере) перевесили теоретическую строгость.

  2. Какая критическая проблема возникает при кэшировании ответов, если версия передается в заголовке X-API-Version? Ответ: Если промежуточный кэш-сервер (CDN Cloudflare, NGINX) кэширует ответ только по URL, то первый клиент запросит версию v1, а второй клиент с заголовком v2 получит из кэша устаревший ответ v1. Для предотвращения сервер обязан передавать заголовок Vary: X-API-Version.

  3. Что такое паттерн Expand and Contract и как он защищает базы данных при смене версий API? Ответ: Это метод изменения схемы БД без даунтайма: на этапе Expand в таблицу добавляются новые колонки рядом со старыми; на этапе параллельной работы приложение пишет в обе колонки; после перевода всех клиентов на новую версию API на этапе Contract старые колонки безопасно удаляются.

  4. Что делать, если в мобильном приложении старая версия 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 прокси отдаст кэш одной версии клиенту другой версии).

Связанные темы