Flask API Versioning
As your API grows and evolves, changes to endpoints, parameters, and response formats become inevitable. But what happens to existing clients who depend on your API's current behavior? This is where API versioning comes in - allowing you to introduce new features and changes while maintaining backward compatibility with existing clients.
Why Version Your API?
Before diving into implementation, let's understand why API versioning is crucial:
- Backwards compatibility: Allows existing clients to continue functioning even as your API evolves
- Gradual migration: Gives clients time to update their code to work with newer versions
- Documentation clarity: Makes it clear which features are available in which versions
- Controlled deprecation: Allows you to phase out old functionality over time
Common Versioning Strategies
There are several approaches to API versioning in Flask:
- URI Path Versioning: Including the version in the URL path (e.g.,
/api/v1/users
) - Query Parameter Versioning: Specifying version via query parameter (e.g.,
/api/users?version=1
) - Header-Based Versioning: Using custom HTTP headers (e.g.,
Accept-Version: v1
) - Accept Header Versioning: Using content negotiation via the
Accept
header (e.g.,Accept: application/vnd.myapi.v1+json
)
Let's explore each approach with practical examples.
URI Path Versioning
This is the most straightforward approach and is widely used due to its simplicity and visibility.
Implementation
from flask import Flask, jsonify, Blueprint
app = Flask(__name__)
# Create blueprints for different versions
v1_blueprint = Blueprint('v1', __name__, url_prefix='/api/v1')
v2_blueprint = Blueprint('v2', __name__, url_prefix='/api/v2')
# V1 endpoint
@v1_blueprint.route('/users/<int:user_id>')
def get_user_v1(user_id):
# V1 implementation
return jsonify({
'id': user_id,
'name': 'John Doe',
'email': '[email protected]'
})
# V2 endpoint with additional fields
@v2_blueprint.route('/users/<int:user_id>')
def get_user_v2(user_id):
# V2 implementation with more data
return jsonify({
'id': user_id,
'name': 'John Doe',
'email': '[email protected]',
'role': 'user',
'created_at': '2023-01-01T00:00:00Z'
})
# Register blueprints
app.register_blueprint(v1_blueprint)
app.register_blueprint(v2_blueprint)
if __name__ == '__main__':
app.run(debug=True)
Example Requests and Responses
V1 Request:
GET /api/v1/users/123 HTTP/1.1
Host: localhost:5000
V1 Response:
{
"id": 123,
"name": "John Doe",
"email": "[email protected]"
}
V2 Request:
GET /api/v2/users/123 HTTP/1.1
Host: localhost:5000
V2 Response:
{
"id": 123,
"name": "John Doe",
"email": "[email protected]",
"role": "user",
"created_at": "2023-01-01T00:00:00Z"
}
Pros and Cons of URI Versioning
Pros:
- Easy to implement and understand
- Highly visible to developers
- Simple to document and test
- Works with caching systems and browsers
Cons:
- URLs change with each version
- Not as clean from a RESTful design perspective (resource should be identified by a single URI)
Query Parameter Versioning
This approach keeps the same URI but uses a query parameter to specify the version.
Implementation
from flask import Flask, jsonify, request
app = Flask(__name__)
@app.route('/api/users/<int:user_id>')
def get_user(user_id):
# Get version from query parameter, default to v1
version = request.args.get('version', '1')
# Base response
response = {
'id': user_id,
'name': 'John Doe',
'email': '[email protected]'
}
# Add additional fields for v2
if version == '2':
response.update({
'role': 'user',
'created_at': '2023-01-01T00:00:00Z'
})
return jsonify(response)
if __name__ == '__main__':
app.run(debug=True)
Example Requests and Responses
V1 Request:
GET /api/users/123?version=1 HTTP/1.1
Host: localhost:5000
V2 Request:
GET /api/users/123?version=2 HTTP/1.1
Host: localhost:5000
Pros and Cons of Query Parameter Versioning
Pros:
- Same URI for all versions
- Easy to implement
- Easy to default to a specific version
Cons:
- Can be overlooked in documentation
- Might interfere with other query parameters
- May cause issues with caching
Header-Based Versioning
With this approach, you specify the version using a custom HTTP header.
Implementation
from flask import Flask, jsonify, request
app = Flask(__name__)
@app.route('/api/users/<int:user_id>')
def get_user(user_id):
# Get version from custom header, default to v1
version = request.headers.get('API-Version', 'v1')
# Base response
response = {
'id': user_id,
'name': 'John Doe',
'email': '[email protected]'
}
# Add additional fields for v2
if version == 'v2':
response.update({
'role': 'user',
'created_at': '2023-01-01T00:00:00Z'
})
return jsonify(response)
if __name__ == '__main__':
app.run(debug=True)
Example Requests and Responses
V1 Request:
GET /api/users/123 HTTP/1.1
Host: localhost:5000
API-Version: v1
V2 Request:
GET /api/users/123 HTTP/1.1
Host: localhost:5000
API-Version: v2
Pros and Cons of Header-Based Versioning
Pros:
- Clean URIs that don't change between versions
- Follows HTTP protocol design
- Separates versioning concerns from resource identification
Cons:
- Less visible, may be harder to discover
- Not as easily testable in a browser
- Requires more sophisticated API clients
Accept Header Versioning
This approach uses content negotiation through the standard Accept
header.
Implementation
from flask import Flask, jsonify, request, abort
app = Flask(__name__)
@app.route('/api/users/<int:user_id>')
def get_user(user_id):
accept_header = request.headers.get('Accept', '')
# Base response
response = {
'id': user_id,
'name': 'John Doe',
'email': '[email protected]'
}
# Handle versioning using Accept header
if 'application/vnd.myapi.v2+json' in accept_header:
response.update({
'role': 'user',
'created_at': '2023-01-01T00:00:00Z'
})
elif 'application/vnd.myapi.v1+json' in accept_header or '*/*' in accept_header:
pass # Use default v1 response
else:
# If client requests unsupported version
abort(406) # Not Acceptable
return jsonify(response)
if __name__ == '__main__':
app.run(debug=True)
Example Requests and Responses
V1 Request:
GET /api/users/123 HTTP/1.1
Host: localhost:5000
Accept: application/vnd.myapi.v1+json
V2 Request:
GET /api/users/123 HTTP/1.1
Host: localhost:5000
Accept: application/vnd.myapi.v2+json
Pros and Cons of Accept Header Versioning
Pros:
- Follows HTTP content negotiation standards
- Clean URIs
- Semantically correct - requesting different representations of the same resource
Cons:
- More complex to implement
- Less discoverable and harder to test
- Requires more sophisticated API clients
Creating a Flexible Version Manager
For larger applications, you might want to create a more structured approach to version handling:
from flask import Flask, request, jsonify, Blueprint
from functools import wraps
app = Flask(__name__)
# Version decorator
def version_required(min_version):
def decorator(f):
@wraps(f)
def decorated_function(*args, **kwargs):
# Get version from URI path (/api/v1/...)
version_str = request.blueprint.split('_')[1] if request.blueprint else 'v1'
version_num = int(version_str[1:])
if version_num < min_version:
return jsonify({
'error': f'This endpoint requires API version {min_version} or higher'
}), 400
return f(*args, **kwargs)
return decorated_function
return decorator
# Create blueprints
v1_blueprint = Blueprint('api_v1', __name__, url_prefix='/api/v1')
v2_blueprint = Blueprint('api_v2', __name__, url_prefix='/api/v2')
# Endpoints available in all versions
@v1_blueprint.route('/products')
def get_products_v1():
return jsonify([
{'id': 1, 'name': 'Product 1', 'price': 10.99}
])
@v2_blueprint.route('/products')
def get_products_v2():
return jsonify([
{'id': 1, 'name': 'Product 1', 'price': 10.99, 'category': 'Electronics'}
])
# Endpoint only available from v2
@v2_blueprint.route('/categories')
@version_required(2) # Requires at least v2
def get_categories():
return jsonify([
{'id': 1, 'name': 'Electronics'}
])
app.register_blueprint(v1_blueprint)
app.register_blueprint(v2_blueprint)
if __name__ == '__main__':
app.run(debug=True)
This approach allows you to restrict certain endpoints to specific API versions, making it clear which functionality is available in which version.
Real-World Example: E-commerce API
Let's consider a real-world e-commerce API that evolves over time:
from flask import Flask, jsonify, Blueprint, request
from datetime import datetime
app = Flask(__name__)
# Create versioned blueprints
v1_api = Blueprint('v1_api', __name__, url_prefix='/api/v1')
v2_api = Blueprint('v2_api', __name__, url_prefix='/api/v2')
# Mock database
products = [
{
'id': 1,
'name': 'Laptop',
'price': 999.99,
'category': 'Electronics',
'stock': 50,
'created_at': '2023-01-15T10:00:00Z'
},
{
'id': 2,
'name': 'Smartphone',
'price': 699.99,
'category': 'Electronics',
'stock': 100,
'created_at': '2023-02-20T09:30:00Z'
}
]
# V1 API - Basic product listing
@v1_api.route('/products')
def get_products_v1():
# V1 only returns basic product info
simplified_products = [
{'id': p['id'], 'name': p['name'], 'price': p['price']}
for p in products
]
return jsonify(simplified_products)
# V1 API - Get product by ID
@v1_api.route('/products/<int:product_id>')
def get_product_v1(product_id):
product = next((p for p in products if p['id'] == product_id), None)
if not product:
return jsonify({'error': 'Product not found'}), 404
# V1 only returns basic product info
return jsonify({
'id': product['id'],
'name': product['name'],
'price': product['price']
})
# V2 API - Enhanced product listing with filtering
@v2_api.route('/products')
def get_products_v2():
category = request.args.get('category')
min_price = request.args.get('min_price', type=float)
max_price = request.args.get('max_price', type=float)
filtered_products = products
# Apply filters if provided
if category:
filtered_products = [p for p in filtered_products if p['category'] == category]
if min_price is not None:
filtered_products = [p for p in filtered_products if p['price'] >= min_price]
if max_price is not None:
filtered_products = [p for p in filtered_products if p['price'] <= max_price]
# V2 returns more detailed product info
return jsonify([
{
'id': p['id'],
'name': p['name'],
'price': p['price'],
'category': p['category'],
'stock': p['stock'],
'created_at': p['created_at']
}
for p in filtered_products
])
# V2 API - Get product by ID with more details
@v2_api.route('/products/<int:product_id>')
def get_product_v2(product_id):
product = next((p for p in products if p['id'] == product_id), None)
if not product:
return jsonify({'error': 'Product not found'}), 404
# V2 returns all product details
return jsonify(product)
# New endpoint only available in V2
@v2_api.route('/products/search')
def search_products():
query = request.args.get('q', '').lower()
if not query:
return jsonify({'error': 'Search query required'}), 400
matches = [p for p in products if query in p['name'].lower()]
return jsonify(matches)
# Register blueprints
app.register_blueprint(v1_api)
app.register_blueprint(v2_api)
if __name__ == '__main__':
app.run(debug=True)
In this example:
- V1 API provides basic product information and simple endpoints
- V2 API adds:
- More detailed product information
- Filtering capabilities
- A new search endpoint
- More efficient querying options
This allows existing clients to continue using the V1 API while new clients can take advantage of the enhanced features in V2.
Best Practices for API Versioning
- Start with versioning from day one - Even if you only have a v1, having the structure in place makes future versions easier to add
- Don't change existing versions - Once a version is published, treat it as immutable
- Document deprecation plans - Clearly communicate when older versions will be retired
- Use semantic versioning - Major version changes for breaking changes, minor for non-breaking additions
- Make new versions backward compatible when possible - Try to support old parameters and behavior
- Test all active versions - Ensure all supported versions continue to work
- Consider versioning at the resource level - Not all endpoints need to change with each version
Summary
API versioning is a crucial aspect of maintaining and evolving your Flask API over time. We've explored several approaches:
- URI Path Versioning: Simple and visible but changes resource URIs
- Query Parameter Versioning: Maintains consistent URIs but can be less visible
- Header-Based Versioning: Clean from a REST perspective but harder to test
- Accept Header Versioning: Follows HTTP standards but more complex to implement
Each approach has its merits, and the right choice depends on your specific requirements. Many APIs use a combination of these techniques.
No matter which approach you choose, having a well-defined versioning strategy from the start will help you evolve your API while maintaining backward compatibility for existing clients.
Further Learning and Exercises
- Exercise: Convert an existing Flask API to use versioning with Blueprints
- Challenge: Implement a hybrid versioning system that supports both URI and header-based versioning
- Project: Create a simple API with two versions, demonstrating a major change between them
Additional Resources
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)