Реализация версионирования 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 без try? падает с 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.







