🌐 Розділ 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 у внутрішній виклик або спрямовує на кластер подів 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? Відповідь: Це метод зміни схеми БД без простою (zero-downtime): на етапі 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 проксі віддасть кеш однієї версії клієнту іншої версії).

Пов’язані теми