- 1. URI-based versioning (e.g., /v1/users) is clearer than header-based versioning
- 2. Additive changes (new fields, endpoints) don’t require version bumps
- 3. Breaking changes (removing fields, changing types) require new major versions
- 4. Deprecation policies should give clients 12-18 months to migrate
The Versioning Dilemma
After managing 150+ production APIs, we’ve seen every versioning strategy. Header-based, query parameter-based, URI-based, content negotiation. They all work, but URI-based versioning wins for simplicity and clarity.
/v1/users vs /v2/users is immediately obvious. No guessing about headers. Works with any HTTP client. Easy to cache and route.
Breaking vs Additive Changes
Not every change requires a version bump. Understanding the difference between breaking and additive changes is critical to maintaining backward compatibility.
Additive Changes (No Version Bump)
- 1. Adding new endpoints:
/v1/orders/export - 2. Adding optional request fields:
filters?: string[] - 3. Adding new response fields (clients ignore unknown fields)
- 4. Making required fields optional
Breaking Changes (Require New Version)
- 1. Removing endpoints or fields
- 2. Changing field types:
age: string → number - 3. Making optional fields required
- 4. Changing authentication schemes
- 5. Altering HTTP status codes for existing errors
Deprecation Strategy
- 1. +0 months: Release v2, announce v1 deprecation
- 2. +3 months: Add
Sunsetheader to v1 responses - 3. +6 months: Email all v1 clients with migration guide
- 4. +12 months: Final warning, set shutdown date
- 5. +18 months: Decommission v1
Version Discovery
Make it easy for clients to discover supported versions. Add a /versions endpoint:
GET /versions
{
"versions": [
{
"version": "v2",
"status": "current",
"released": "2024-01-15"
},
{
"version": "v1",
"status": "deprecated",
"sunset": "2025-07-15",
"released": "2023-01-10"
}
]
}
Common Pitfalls
- Micro-Versioning: Don’t create v1.1, v1.2, v1.3. Stick to major versions only for simplicity.
- Silent Breaking Changes: Always document what changed between versions in your changelog.
- No Migration Path: Provide migration guides, code examples, and automated tools when possible.
Supporting Multiple Versions
How do you maintain v1 and v2 simultaneously without duplicating all your business logic? We use a versioned adapter pattern: shared domain logic with version-specific DTOs and transformers.
The controller routes to different adapter classes based on version. Each adapter transforms the shared domain model to its version-specific response format.