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