APIs are contracts. Once published, they’re hard to change. Add a field, and existing clients work fine. Remove a field, and existing clients break. The challenge: evolve your API to meet new requirements without breaking the consumers who depend on current behavior.
Versioning provides the solution—multiple API versions coexisting, enabling evolution while maintaining compatibility.
Versioning Strategies
URL Path Versioning
Include version in the URL path:
GET /v1/users/123
GET /v2/users/123
Advantages:
- Highly visible and explicit
- Easy to route at infrastructure level
- Clear which version clients use
- Simple to cache (different URLs)
Disadvantages:
- URL changes for every version
- Clients must update URLs for upgrades
URL versioning is the most common approach because of its clarity.
Query Parameter Versioning
Include version as query parameter:
GET /users/123?version=1
GET /users/123?version=2
Advantages:
- Same URLs, different behavior
- Easy to default (unspecified = latest or oldest)
Disadvantages:
- Less visible in URLs
- Caching complexity
- Easy to forget version parameter
Header Versioning
Include version in request headers:
GET /users/123
Accept: application/vnd.myapi.v1+json
Or custom header:
GET /users/123
API-Version: 1
Advantages:
- Clean URLs
- Follows HTTP content negotiation principles
Disadvantages:
- Less visible—can’t see version in URL
- Harder for developers testing in browsers
- Some proxies don’t preserve custom headers
No Explicit Versioning
Make all changes backward compatible. Never break existing clients.
Advantages:
- No version complexity
- Clients don’t need to upgrade
Disadvantages:
- Constrains evolution—some changes are inherently breaking
- Accumulates cruft as old patterns persist
Works for simple APIs with limited evolution needs. Becomes difficult as APIs mature.
What Constitutes a Breaking Change
Understanding what breaks clients helps avoid accidental breakage.
Breaking Changes
- Removing a field from response
- Removing an endpoint
- Changing field type (string to number)
- Changing required parameters
- Changing error response format
- Changing authentication requirements
- Changing resource identifiers
Non-Breaking Changes
- Adding new fields to response
- Adding new optional parameters
- Adding new endpoints
- Adding new error codes (if clients handle unknown codes gracefully)
- Performance improvements
- Bug fixes
Gray Areas
- Changing field meaning (same type, different semantics)
- Changing limits or quotas
- Adding required fields (breaking for creation, not retrieval)
Document your compatibility policy so consumers know what to expect.
Deprecation Strategy
Deprecation bridges old and new versions:
Mark Deprecated
Signal that features will be removed:
{
"user_id": 123,
"email": "user@example.com",
"legacyId": "abc123" // Deprecated, use user_id
}
Response headers can indicate deprecation:
Deprecation: true
Sunset: Sat, 31 Dec 2017 23:59:59 GMT
Link: </docs/migration>; rel="deprecation"
Migration Period
Give consumers time to migrate:
- Announce: Communicate deprecation with timeline
- Overlap: Run old and new simultaneously
- Monitor: Track usage of deprecated features
- Communicate: Notify heavy users directly
- Remove: Only after migration period ends
Migration periods depend on your API’s consumer profile. Public APIs need longer periods than internal APIs.
Sunset Policy
Document your sunset policy:
- Minimum notice period before removal (e.g., 6 months)
- How deprecation is communicated
- What support is available during transition
- Exceptions process
Clear policy sets expectations and builds trust.
Implementation Patterns
Version per Route
Different route handlers for each version:
@app.route('/v1/users/<id>')
def get_user_v1(id):
return UserV1Schema().dump(get_user(id))
@app.route('/v2/users/<id>')
def get_user_v2(id):
return UserV2Schema().dump(get_user(id))
Simple but duplicates code.
Transformation Layer
Single implementation with version-specific transformation:
@app.route('/v<version>/users/<id>')
def get_user(version, id):
user = get_user_from_db(id)
return transform_for_version(user, version)
def transform_for_version(user, version):
if version == '1':
return UserV1Schema().dump(user)
elif version == '2':
return UserV2Schema().dump(user)
Centralizes logic but transformation layer can become complex.
Feature Flags
Version-like behavior via feature flags:
def get_user(id, include_new_fields=False):
user = get_user_from_db(id)
response = basic_user_schema(user)
if include_new_fields:
response['new_field'] = user.new_field
return response
More granular than version numbers but can become complex.
Best Practices
Version Early
Don’t wait until you need breaking changes to think about versioning. Include version from the start, even if you only have v1.
Document Compatibility
Be explicit about what compatibility guarantees you provide:
- What’s a breaking change
- Deprecation policy
- Sunset timelines
- How clients should handle unknown fields
Monitor Version Usage
Track which versions consumers use:
- How many requests per version
- Who’s using deprecated versions
- Are users migrating over time
Data informs deprecation decisions.
Provide Migration Guides
When releasing new versions, provide:
- What changed and why
- How to migrate
- Code examples
- Timeline expectations
Good documentation smooths migration.
Limit Active Versions
Supporting many versions simultaneously is expensive. Have a policy:
- Support current version and one previous
- Sunset older versions after specified period
- Exceptional support for enterprise customers if needed
Test Across Versions
Test that all supported versions work correctly. Version-specific bugs erode trust.
Key Takeaways
- URL path versioning is most common due to visibility and simplicity
- Breaking changes include removing fields, changing types, and changing requirements
- Non-breaking changes can be made without versioning (adding fields, endpoints)
- Deprecation requires clear communication, migration periods, and sunset policies
- Monitor version usage to inform deprecation decisions
- Limit active versions to reduce maintenance burden
- Provide migration guides and test across all supported versions