APIs are contracts. Breaking changes break trust. But APIs must evolve—new features, better designs, deprecated endpoints. The question isn’t whether to change, but how to change without leaving clients stranded.
Why Versioning Matters
Without versioning, you have two bad options:
- Never change: Your API calcifies, accumulating cruft forever
- Change freely: Clients break unexpectedly, trust erodes
Versioning gives you a third path: evolve deliberately, with clear communication and migration windows.
Versioning Strategies
URL Path Versioning
The most explicit approach—version in the URL:
Implementation:
| |
Pros:
- Explicit and visible
- Easy to route and test
- Clear in documentation and logs
Cons:
- URL changes when version changes
- Can lead to code duplication
- Caching varies by URL
Header Versioning
Version in a custom header:
Or using content negotiation:
Implementation:
| |
Pros:
- Clean URLs
- Same resource, different representations
- Better REST semantics
Cons:
- Less visible/discoverable
- Harder to test in browser
- Easy to forget the header
Query Parameter Versioning
Version as a query parameter:
Pros:
- Easy to implement
- Visible in URLs
- Optional with defaults
Cons:
- Pollutes query string
- Can conflict with other parameters
- Feels like a workaround
My Recommendation: URL Versioning
For most APIs, URL path versioning wins:
- Explicit is better than implicit
- Easy to document, test, and debug
- Clear routing in infrastructure (load balancers, API gateways)
- Clients know exactly what they’re calling
Reserve header versioning for APIs where URL stability matters more than explicitness (e.g., hypermedia APIs).
What Counts as a Breaking Change?
Breaking (requires new version):
- Removing a field from response
- Removing an endpoint
- Changing field type (string → number)
- Changing required/optional status
- Changing authentication requirements
- Changing error response format
Non-breaking (safe in current version):
- Adding new optional fields to response
- Adding new endpoints
- Adding new optional parameters
- Adding new enum values (if clients handle unknown values)
- Performance improvements
- Bug fixes
Backward-Compatible Evolution
Before jumping to a new version, try evolving compatibly:
Adding Fields
Safe—old clients ignore new fields:
| |
Deprecating Fields
Mark as deprecated, keep returning them:
| |
Or use response headers:
Optional Parameters with Defaults
Add new functionality via optional parameters:
Version Lifecycle
Define clear lifecycle stages:
Current: Active development, new features Stable: Maintained, security fixes only Deprecated: Sunset date announced, migration docs available Sunset: Removed, returns 410 Gone
Communicate Clearly
| |
Implementation Patterns
Shared Core, Version Adapters
Don’t duplicate business logic:
| |
API Gateway Routing
Let infrastructure handle version routing:
| |
Testing Across Versions
Contract tests ensure versions behave correctly:
| |
Common Mistakes
Too many versions: Three concurrent versions is manageable. Ten is a maintenance nightmare.
No sunset policy: Old versions accumulate forever. Set expiration dates.
Breaking changes in minor versions: If you’re on v2, don’t break v2 clients. That’s what v3 is for.
No migration guide: Announcing deprecation without explaining how to migrate is hostile.
Versioning too early: Don’t ship v2 until v1 is actually limiting you.
The Mental Model
Think of API versions like software releases:
- Major version (v1 → v2): Breaking changes, migration required
- Minor updates within version: Backward-compatible additions
- Deprecation period: Like end-of-life announcements
- Sunset: Like decommissioning old software
Your API is a product. Versions are releases. Treat them with the same discipline you’d treat shipping software.
The goal isn’t to never change—it’s to change predictably, communicate clearly, and give clients time to adapt.