API versioning is crucial for evolving APIs without breaking existing clients. This guide covers different strategies, when to use each, and implementation patterns.
Versioning Strategies Overview
strategies:
- name: "URL Path"
example: "/api/v1/users"
pros: "Simple, visible, cacheable"
cons: "URL pollution, duplicate endpoints"
- name: "Query Parameter"
example: "/api/users?version=1"
pros: "Single endpoint, flexible"
cons: "Harder to cache, less explicit"
- name: "Header"
example: "Accept: application/vnd.api.v1+json"
pros: "Clean URLs, flexible"
cons: "Less visible, harder to test"
- name: "Content Negotiation"
example: "Accept: application/vnd.myapp.v1+json"
pros: "Standard HTTP, clean"
cons: "Complex, versioning in MIME type"
URL Path Versioning
Implementation
# Flask URL versioning
from flask import Flask, Blueprint, jsonify, request
app = Flask(__name__)
# Version 1 blueprint
v1_bp = Blueprint('v1', __name__, url_prefix='/api/v1')
@v1_bp.route('/users')
def get_users_v1():
return jsonify({
'users': [
{'id': 1, 'name': 'John', 'email': '[email protected]'}
]
})
@v1_bp.route('/users/<int:user_id>')
def get_user_v1(user_id):
return jsonify({
'id': user_id,
'name': 'John',
'email': '[email protected]'
})
# Version 2 blueprint
v2_bp = Blueprint('v2', __name__, url_prefix='/api/v2')
@v2_bp.route('/users')
def get_users_v2():
# More fields in v2
return jsonify({
'users': [
{
'id': 1,
'name': 'John',
'email': '[email protected]',
'profile': {'avatar': 'url', 'bio': 'Bio'},
'settings': {'theme': 'dark'}
}
]
})
# Register blueprints
app.register_blueprint(v1_bp)
app.register_blueprint(v2_bp)
Django REST Framework
# Django URL versioning
from rest_framework import viewsets
from rest_framework.versioning import URLPathVersioning
class UserViewSet(viewsets.ModelViewSet):
versioning_class = URLPathVersioning
def get_serializer_class(self):
if self.request.version == 'v1':
return UserSerializerV1
return UserSerializerV2
# urls.py
from django.urls import path, include
urlpatterns = [
path('api/v1/', include((router.urls, 'api'), namespace='v1')),
path('api/v2/', include((router.urls, 'api'), namespace='v2')),
]
Header Versioning
Custom Header Implementation
# Header-based versioning
from flask import Flask, request, jsonify
from functools import wraps
app = Flask(__name__)
def versionize(versions):
"""Decorator to route to correct version handler"""
def decorator(f):
@wraps(f)
def decorated_function(*args, **kwargs):
version = request.headers.get('X-API-Version', 'v1')
version_handler = versions.get(version)
if not version_handler:
version_handler = versions.get('v1') # Default
return version_handler(*args, **kwargs)
return decorated_function
return decorator
@app.route('/api/users')
@versionize({
'v1': lambda: jsonify({'users': [{'name': 'John'}]}),
'v2': lambda: jsonify({'users': [{'name': 'John', 'avatar': 'url'}]})
})
def get_users():
pass
Accept Header Versioning
# Using Accept header with MIME types
@app.route('/api/users')
def get_users():
# Parse Accept header
accept = request.headers.get('Accept', '')
# Check for versioned media type
if 'application/vnd.api.v1+json' in accept:
return jsonify({'version': 'v1', 'data': []})
elif 'application/vnd.api.v2+json' in accept:
return jsonify({'version': 'v2', 'data': [], 'metadata': {}})
# Default or fallback
return jsonify({'version': 'default', 'data': []})
# Client request
headers = {
'Accept': 'application/vnd.api.v2+json'
}
response = requests.get('/api/users', headers=headers)
Query Parameter Versioning
# Query parameter versioning
@app.route('/api/users')
def get_users():
version = request.args.get('version', '1')
if version == '1':
return jsonify({'users': [{'id': 1, 'name': 'John'}]})
elif version == '2':
return jsonify({
'users': [{
'id': 1,
'name': 'John',
'profile': {}
}]
})
return jsonify({'error': 'Invalid version'}), 400
Version Deprecation
Handling Deprecated Versions
# Version deprecation handler
from datetime import datetime, timedelta
class VersionDeprecation:
"""Handle API version deprecation"""
def __init__(self):
self.versions = {
'v1': {
'deprecated': True,
'sunset_date': '2025-06-01',
'warning_header': 'X-API-Warning'
},
'v2': {
'deprecated': False,
'sunset_date': None
}
}
def check_version(self, version):
if version not in self.versions:
return {
'error': f'Unknown version: {version}',
'supported': list(self.versions.keys())
}, 400
info = self.versions[version]
if info['deprecated']:
sunset = datetime.fromisoformat(info['sunset_date'])
if datetime.now() > sunset:
return {
'error': f'Version {version} no longer supported',
'sunset_date': info['sunset_date']
}, 410 # Gone
return None, None
def add_deprecation_headers(self, response, version):
info = self.versions.get(version, {})
if info.get('deprecated'):
response.headers['Deprecation'] = 'true'
response.headers['Link'] = f'<{self.get_current_version_uri()}>; rel="successor-version"'
if info.get('sunset_date'):
response.headers['Sunset'] = info['sunset_date']
return response
@app.route('/api/users')
def get_users():
version = request.args.get('version', 'v2')
checker = VersionDeprecation()
error, status = checker.check_version(version)
if error:
return jsonify(error), status
# Get data...
response = jsonify({'users': []})
return checker.add_deprecation_headers(response, version)
Best Practices
# API versioning best practices
when_to_version:
- "Breaking changes to response format"
- "Removing fields"
- "Changing field types"
- "Adding required fields"
strategies:
- "URL path: Most explicit, good for public APIs"
- "Header: Clean URLs, good for private APIs"
- "Query: Simple but less visible"
deprecation:
- "Announce deprecations early"
- "Use Sunset header"
- "Provide migration guide"
- "Set timeline (6-12 months)"
advice:
- "Don't version on every change"
- "Use semantic versioning"
- "Document supported versions"
- "Monitor version usage"
Conclusion
Choose versioning strategy based on your needs:
- URL path: Most visible, good for public APIs
- Header: Clean URLs, flexible
- Query parameter: Simple but less explicit
Always plan for deprecation and communicate changes early.
Comments