API Versioning & Backward Compatibility
APIs evolve. New features require new fields, business logic changes, and old designs need fixing. But here's the constraint: existing clients can't break. You might have thousands of integrations running in production — each pinned to your current API contract. How do you move forward without leaving them behind?
Why Versioning Matters
Unlike internal code where you can refactor freely, a public API is a contract. When you change it:
- Mobile apps in app stores can't be force-updated instantly
- Third-party integrations may not have active maintainers
- Enterprise clients have change-control processes that take months
- Breaking a partner's integration costs trust (and revenue)
The goal: evolve your API continuously while giving clients time and tools to migrate.
Versioning Strategies
Three Approaches
| Strategy | Example | Pros / Cons |
|---|---|---|
| URL path | /v1/users /v2/users |
✅ Obvious, easy to route ❌ Pollutes URL namespace, hard to deprecate gradually |
| Query param | /users?version=2 | ✅ Optional, single URL ❌ Easy to forget, caching complications |
| Header | Accept: application/vnd.api+json;version=2 Stripe-Version: 2024-01-15 |
✅ Clean URLs, per-request override ❌ Less discoverable, harder to test in browser |
Backward Compatible vs Breaking Changes
✅ Safe (Backward Compatible)
- Adding new fields to responses (clients ignore unknown fields)
- Adding new endpoints (doesn't affect existing routes)
- Adding optional parameters (existing requests still work without them)
- Adding new enum values (if clients handle unknown gracefully)
- Widening a type (int32 → int64, if wire format allows)
❌ Dangerous (Breaking)
- Removing fields from responses — clients parsing them will crash
- Renaming fields — same as removing + adding
- Changing field types —
"id": 42→"id": "uuid-abc" - Removing endpoints — 404 for active clients
- Making optional params required — existing requests become invalid
- Changing error formats — client error handling breaks
Deprecation Strategy
The Four-Phase Sunset
- Announce: Document the deprecation, email affected developers, add to changelog
- Sunset Header: Add
Sunset: Sat, 01 Jun 2024 00:00:00 GMTheader to deprecated responses - Grace Period: Keep the old version running for 6-12 months minimum. Log usage to track remaining clients.
- Remove: After the sunset date, return
410 Gonewith a migration guide link
HTTP/1.1 200 OK
Sunset: Sat, 01 Jun 2024 00:00:00 GMT
Deprecation: true
Link: <https://api.example.com/docs/migration-v2>; rel="deprecation"
Evolution Without Versioning
The Best Version Is No Version
If you design carefully from the start, you can often avoid versioning entirely:
- Additive-only changes: Only add fields, never remove or rename
- Optional fields with defaults: New params default to previous behavior
- Feature flags: New capabilities gated behind opt-in headers
- Tolerant readers: Document that clients MUST ignore unknown fields
- Expand/contract pattern: Add new field → migrate clients → remove old field (over months)
🌍 Stripe's Versioning Model
Stripe uses date-based versions (e.g., 2024-01-15) with a unique approach:
- Each Stripe account is pinned to the API version from when it was created
- Developers can override per-request with the
Stripe-Versionheader to test newer versions - When ready, they upgrade their account's pinned version in the dashboard
- Internally, Stripe maintains a version compatibility layer — a chain of transformations that converts between any two versions. Old versions are never truly removed.
- Each version change is documented with exact migration steps
This means a client from 2019 still works today without any changes — Stripe's gateway transparently converts between versions.
🌍 Slack's Backward Compatibility
Slack maintains backward compatibility across thousands of third-party integrations:
- They almost never remove fields — deprecated fields return empty/default values
- New features use new method names (additive approach)
- Breaking changes get 18+ months notice
- They maintain a "app directory" that lets them contact affected developers directly
Consumer-Driven Contract Testing
Pact: Don't Break Your Consumers
Consumer-driven contracts flip the testing model: instead of the API provider defining tests, each consumer defines what they expect from the API:
// Consumer (mobile app) defines its contract:
{
"description": "get user profile",
"request": { "method": "GET", "path": "/users/42" },
"response": {
"status": 200,
"body": {
"id": 42, // I need this
"name": "any", // I need this
"email": "any" // I need this
}
}
}
// Provider runs ALL consumer contracts on every deploy
// If any break → deploy is blocked
Tools like Pact collect contracts from all consumers and verify them against the provider on every CI run. You know before deploying if a change will break any client.
Interactive: Breaking or Safe?
🎮 Compatibility Checker
For each proposed API change, decide: is it backward compatible (safe) or breaking? Then see the explanation.