API Versioning Implementation for Mobile App Backward Compatibility
Mobile apps sit in App Store and Google Play weeks after a new version releases. Users update slowly: 2 weeks post-release often finds 30-40% of users still on the previous version, and 5-10% on versions two months old. If the backend breaks API without considering old clients, those users see crashes or blank screens. API versioning isn't about RESTful perfectionism — it's commercial necessity.
Versioning Strategies: URL vs Header vs Parameter
Three common approaches, each with trade-offs:
URL versioning (/api/v1/orders, /api/v2/orders). Most obvious. Works, caches easily, visible in logs. Downside: many duplicate routes on server, temptation to copy controllers instead of abstracting changes.
Header versioning (Accept: application/vnd.myapp.v2+json). Cleaner from REST purity perspective. Harder to test (curl needs header), caches worse on CDN without Vary: Accept setup.
Parameter (/api/orders?version=2). Don't do this. Clutters URL, breaks REST semantics, parameter easy to forget.
For mobile apps we recommend URL versioning with app version in separate header:
GET /api/v2/orders
X-App-Version: 4.2.1
X-App-Platform: ios
X-App-Version doesn't drive routing but is critical for analytics: see which app versions still hit old endpoints, make deprecation decisions with data.
On the Mobile App Side
Versioning isn't only server work. Client must correctly handle different API versions during gradual migration.
Basic pattern — API Client with configurable base URL version:
// 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")
// ...
}
}
This lets you switch config in one place when v3 launches, not change URLs throughout code.
Handling Changes on the Client
Most common mistake: strict JSON deserialization without optional fields. Server adds new estimatedDelivery field to /orders response — old client with strict Decodable crashes with keyNotFound. Crash on flat ground.
Correct Codable approach on iOS:
struct Order: Decodable {
let id: String
let status: String
let estimatedDelivery: Date? // Optional — doesn't crash if absent
let legacyField: String? // May disappear in v3 — optional
}
On Android with Gson/Moshi similarly: fields that may absent are nullable types. In Kotlin data class it's explicit: val estimatedDelivery: Date? = null.
Another pattern — Consumer-Driven Contracts via Pact: mobile app publishes contract "I expect these fields in response," CI on backend validates contract on every API change. If backend breaks a field — CI fails before change reaches production.
Deprecation Workflow
Process for retiring old API versions:
- Add header
Deprecation: trueandSunset: Wed, 01 Jan 2026 00:00:00 GMTto old endpoint responses — RFC 8594 standard - Mobile app reads this header and logs warning (or shows "update app" banner)
- Monitor: via
X-App-Versionwatch if users on old app versions still hit deprecated endpoint - Only when traffic to deprecated endpoint < 0.1% — turn it off
Minimum deprecation window for mobile: 3-6 months. Mobile clients don't update as fast as web.
Timeline for implementing API versioning system for existing app: 3 to 6 weeks — audit current endpoints, refactor client code, set up version monitoring. For new project — bake in from first sprint, no overhead.







