Реалізація версіонування API для зворотної сумісності мобільної додатка
Мобільні додатки живуть у App Store та Google Play тижнями після виходу нової версії. Користувачі оновлюються повільно: через 2 тижні після релізу часто 30-40% аудиторії все ще на попередній версії, а 5-10% — на версії двомісячної давності. Якщо бекенд ламає API без огляду на старих клієнтів — ці користувачі бачать крещі або порожні екрани. Версіонування API — це не про RESTful-перфекціонізм, а про комерційну необхідність.
Стратегії версіонування: URL vs Header vs Параметр
Три поширених підходи, кожен з власними trade-offs:
URL-версіонування (/api/v1/orders, /api/v2/orders). Найочевидніше. Працює, легко кешується, видно в логах. Недолік: безліч дублюючихся роутів на сервері та спокуса копіювати контролери замість абстрагування змін.
Header-версіонування (Accept: application/vnd.myapp.v2+json). Чистіше з точки зору REST-пуризму. Важче тестувати (curl потрібно передавати заголовок), гірше кешується CDN без налаштування Vary: Accept.
Параметр (/api/orders?version=2). Не робіть так. Засоряє URL, ламає семантику REST, параметр легко забути.
Для мобільних додатків рекомендуємо URL-версіонування з версією додатка у окремому заголовку:
GET /api/v2/orders
X-App-Version: 4.2.1
X-App-Platform: ios
X-App-Version не управляє маршрутизацією, але критичний для аналітики: бачите, які версії додатка ще стукають у старі ендпоінти, й приймаєте рішення про deprecation з даними.
На стороні мобільної додатка
Versioning — це не тільки серверна задача. Клієнт має коректно працювати з різними версіями API при поступовому переході.
Базовий паттерн — API Client з налаштовуваною base URL версією:
// iOS — Swift
struct APIConfiguration {
let baseURL: URL
let version: APIVersion
enum APIVersion: String {
case v1, v2, v3
}
}
class OrdersAPI {
private let config: APIConfiguration
func fetchOrders() async throws -> [Order] {
let url = config.baseURL
.appendingPathComponent(config.version.rawValue)
.appendingPathComponent("orders")
// ...
}
}
Це дозволяє при виході v3 API переключити конфігурацію в одному місці, а не змінювати URL по всьому коду.
Обробка змін на стороні клієнта
Найчастіша помилка — жорстка десеріалізація JSON без урахування опціональних полів. Сервер додав нове поле estimatedDelivery у відповідь /orders — старий клієнт зі строгим Decodable падає з keyNotFound. Це крещ на ровному місці.
Правильний підхід до Codable на iOS:
struct Order: Decodable {
let id: String
let status: String
let estimatedDelivery: Date? // Опціональне — не кращить якщо відсутнє
let legacyField: String? // Може зникнути у v3 — опціональне
}
На Android з Gson/Moshi аналогічно: поля, які можуть відсутній — nullable типи. У Kotlin data class це виражено явно: val estimatedDelivery: Date? = null.
Ще один паттерн — Consumer-Driven Contracts через Pact: мобільна додаток публікує контракт «я чекаю ці поля у відповіді», CI на бекенді валідує контракт при кожній зміні API. Якщо бекенд зламав поле — CI падає до того, як зміна попала у production.
Deprecation workflow
Процес зняття старої версії API:
- Додати заголовок
Deprecation: trueтаSunset: Wed, 01 Jan 2026 00:00:00 GMTу відповіді старих ендпоінтів — стандарт RFC 8594 - Мобільна додаток читає цей заголовок та логує попередження (або показує банер «оновіть додаток»)
- Моніторинг: через
X-App-Versionдивимось, залишились ли користувачі на старій версії додатка, які ще стукають у deprecated ендпоінт - Тільки коли трафік на deprecated ендпоінт < 0.1% — відключаємо
Мінімальний deprecation window для мобільних: 3-6 місяців. Мобільні клієнти не оновлюються так швидко, як веб.
Терміни реалізації системи версіонування API для існуючої додатка: від 3 до 6 тижнів — аудит поточних ендпоінтів, рефакторинг клієнтського коду, налаштування моніторингу версій. Для нового проекту — закладаємо з першого спринту, без overhead.







