API Versioning Strategies — URI, Header, and Query Parameter Approaches
In this tutorial, you'll learn about API Versioning Strategies. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.
API Versioning is the practice of managing changes to your API over time without breaking existing clients, allowing different clients to use different versions of the same endpoint simultaneously.
What You'll Learn
By the end of this tutorial, you will implement four API Versioning strategies (URI path, header, query parameter, media type), choose the right approach for your API, design backward-compatible changes, and deprecate old versions gracefully.
Why It Matters
Without versioning, every API change risks breaking mobile apps, third-party integrations, and internal services that depend on the existing contract. Doda Browser maintains API version compatibility for its public extension API, ensuring thousands of extensions continue to work across updates.
Real-World Use
Stripe's API uses URL versioning (/v1/charges, /v2/charges) with a deprecation timeline of 2+ years. Clients opt into new versions explicitly, and old versions continue working for existing integrations without forced Migration.
Versioning Strategy Comparison
Graph LR subgraph "API Gateway" A[Request] --> B{Routing} end B --> C["/v1/users/"] B --> D["/v2/users/"] B --> E["Accept: app/vnd.API.v2+JSON"] B --> F["?version=2"] C --> G[V1 Handler] D --> H[V2 Handler] E --> H F --> G style B fill:#f90,color:#fff
Different clients can target different API versions simultaneously. The versioning strategy determines how clients specify which version they want.
Table: Versioning Strategies
| Strategy | Example | Pros | Cons |
|---|---|---|---|
| URI Path | /v1/users |
Simple, explicit, cacheable | URL pollution, hard to redirect |
| Header | Accept: vnd.myapp.v2+json |
Clean URLs, RESTful | Harder to test, requires headers |
| Query Param | /users?version=2 |
Easy to implement | Caching issues, not RESTful |
| Media Type | application/v1+json |
Standards-based | Complex, requires parsing |
| Subdomain | v1.api.example.com |
Full isolation | DNS overhead, SSL certs |
Python: URI Versioning with Flask Blueprint
# API_v1.py
# API version 1 endpoints
from Flask import Blueprint, jsonify
API_v1 = Blueprint('API_v1', __name__, URL_prefix='/API/v1')
users_db = [
{'id': 1, 'name': 'Alice', 'email': 'alice"@example".com'},
{'id': 2, 'name': 'Bob', 'email': 'bob"@example".com'},
]
@API_v1.route('/users')
def list_users():
"""V1: Returns basic user information."""
return jsonify({'users': users_db, 'version': '1.0'})
@API_v1.route('/users/<int:user_id>')
def get_user(user_id):
"""V1: Returns user without profile fields."""
user = next((u for u in users_db if u['id'] == user_id), None)
if not user:
return jsonify({'error': 'User not found'}), 404
return jsonify({'user': user, 'version': '1.0'})
# API_v2.py
# API version 2 endpoints with enhanced data
from Flask import Blueprint, jsonify
API_v2 = Blueprint('API_v2', __name__, URL_prefix='/API/v2')
users_db_v2 = [
{
'id': 1, 'name': 'Alice', 'email': 'alice"@example".com',
'profile': {'bio': 'Developer', 'avatar_URL': '/avatars/alice.jpg'},
'created_at': '2026-01-15T10:00:00Z',
'updated_at': '2026-06-20T15:30:00Z',
},
{
'id': 2, 'name': 'Bob', 'email': 'bob"@example".com',
'profile': {'bio': 'Designer', 'avatar_URL': '/avatars/bob.jpg'},
'created_at': '2026-02-01T08:00:00Z',
'updated_at': '2026-06-19T12:00:00Z',
},
]
@API_v2.route('/users')
def list_users():
"""V2: Returns users with profile and timestamps."""
return jsonify({
'data': users_db_v2,
'meta': {'version': '2.0', 'count': len(users_db_v2)},
})
@API_v2.route('/users/<int:user_id>')
def get_user(user_id):
"""V2: Returns user with profile fields and audit timestamps."""
user = next((u for u in users_db_v2 if u['id'] == user_id), None)
if not user:
error_response = {
'error': {
'code': 'NOT_FOUND',
'message': 'User not found',
'request_id': 'req-abc-123',
}
}
return jsonify(error_response), 404
return jsonify({'data': user, 'meta': {'version': '2.0'}})
# app.py
# Flask app with versioned routes
from Flask import Flask, request, jsonify
from API_v1 import API_v1
from API_v2 import API_v2
app = Flask(__name__)
app.Register_blueprint(API_v1)
app.Register_blueprint(API_v2)
@app.route('/API/version')
def get_versions():
"""Return supported API versions."""
return jsonify({
'versions': ['v1', 'v2'],
'latest': 'v2',
'deprecated': ['v1'],
'deprecation_date': '2027-01-01',
'sunset_date': '2027-06-30',
})
if __name__ == '__main__':
app.run(debug=True)
Expected output:
// GET /API/v2/users
{
"data": [{"id": 1, "name": "Alice", "profile": {...}, ...}],
"meta": {"version": "2.0", "count": 2}
}
Node.js: Header-Based Versioning
// header-versioning.js
// Express middleware for Accept header versioning
const Express = require('Express');
const app = Express();
const versionRegex = /^application\/vnd\.myapp\.v(\d+)\+JSON$/;
function versionMiddleware(req, res, next) {
const accept = req.headers['accept'] || '';
const match = accept.match(versionRegex);
if (match) {
req.apiVersion = parseInt(match[1], 10);
} else {
// Default to latest version
req.apiVersion = 2;
}
// Validate version
if (req.apiVersion < 1 || req.apiVersion > 2) {
return res.status(400).JSON({
error: 'Unsupported API version',
supportedVersions: ['v1', 'v2'],
});
}
next();
}
app.use(versionMiddleware);
// Version-aware route handler
app.get('/users', (req, res) => {
if (req.apiVersion === 1) {
return res.JSON({
users: [
{ id: 1, name: 'Alice', email: 'alice"@example".com' },
],
version: '1.0',
});
}
// Version 2 (default)
res.JSON({
data: [
{
id: 1, name: 'Alice', email: 'alice"@example".com',
profile: { bio: 'Developer' },
},
],
meta: { version: '2.0' },
});
});
app.listen(3000);
Deprecation Handling
# deprecation.py
# API deprecation middleware
from Flask import request, g, jsonify
from datetime import datetime, date
import functools
import warnings
DEPRECATED_VERSIONS = {
'v1': {
'sunset': date(2027, 6, 30),
'Migration_URL': '/docs/Migration-v1-to-v2',
'changelog': '/docs/changelog#v2',
}
}
def deprecation_check():
"""Add deprecation warnings to response headers."""
version = request.path.split('/')[2] if '/API/' in request.path else None
if version and version in DEPRECATED_VERSIONS:
info = DEPRECATED_VERSIONS[version]
days_left = (info['sunset'] - date.today()).days
g.deprecation_headers = {
'Sunset': info['sunset'].isoformat(),
'Deprecation': f'True; sunset="{info['sunset']}"; '
f'days_remaining={days_left}',
'Link': f'<{info["Migration_URL"]}>; rel="deprecation"',
}
else:
g.deprecation_headers = {}
@app.after_request
def add_deprecation_headers(response):
"""Attach deprecation headers to response."""
if hasattr(g, 'deprecation_headers'):
for key, value in g.deprecation_headers.items():
response.headers[key] = value
return response
def deprecated(version, alternative):
"""Decorator marking an endpoint as deprecated."""
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
warnings.warn(
f"Endpoint '{func.__name__}' is deprecated in {version}. "
f"Use {alternative} instead.",
DeprecationWarning, stacklevel=2
)
return func(*args, **kwargs)
return wrapper
return decorator
# Usage
@deprecated(version='v1', alternative='/API/v2/users')
def old_users_endpoint():
return jsonify({'users': []})
Common Errors
1. Removing Fields Without Notice
Removing a field from a response breaks clients that depend on it. Use additive changes: add fields but never remove or rename them within the same version. Deprecate fields with a deprecated annotation before removing them in a new version.
2. Changing Error Response Format
Clients parse error responses programmatically. Changing the error schema (field names, structure, status codes) between versions is a breaking change. Maintain consistent error formats within a version.
3. No Version Discovery Endpoint
Clients need a way to discover available versions, their status (current/deprecated/sunset), and Migration guides. Provide a /api/version or /api/versions endpoint that returns this metadata.
4. Breaking Changes in Patch Versions
Semantic versioning for APIs means breaking changes (field removal, type changes) only occur in major versions. Patch and minor versions must be backward-compatible. Non-breaking changes are: adding optional fields, adding new endpoints, and expanding enum values.
5. Not Versioning the Database Schema
When API v2 introduces new fields, the database schema must support both v1 and v2 responses. Use database views, computed columns, or transformation layers to serve old formats without duplicating data.
6. Ignoring Client Identification
Without knowing which client version is calling, you cannot enforce deprecation timelines. Require a User-Agent or X-Client-Version header and log it for analytics. Use this data to decide when to sunset old versions.
Practice Questions
1. Which API Versioning strategy is most commonly used?
URI path versioning (/v1/resource) is the most common because it is explicit, easy to test, cacheable, and visible in every request. It is supported by all HTTP clients without special header configuration.
2. How do you handle breaking changes without versioning?
Add new optional fields without modifying existing ones. Use accept headers for content negotiation within the same endpoint. Add new endpoints instead of changing existing ones. Deprecate old behavior gradually.
3. What is a sunset header and why is it important?
The Sunset HTTP header tells clients when a version will stop being supported. It gives clients a clear deadline for Migration. Combined with the Deprecation header, it provides a complete deprecation timeline.
4. How long should you support old API versions?
Industry standard is 6-24 months depending on the client BASE. Mobile apps update slowly (some users stay on old versions for years). Enterprise APIs often support versions for 2+ years. Communicate deprecation timelines clearly.
5. Challenge: Design an API Versioning system for a social media platform where v1 returns basic user profiles (name, email) and v2 adds profile pictures, bio, followers count, and posts. Implement URI path versioning with Flask or Express. Add a deprecation middleware that returns Sunset headers for v1. Create a version discovery endpoint. Write a Migration guide for v1 to v2 that explains field changes, new error format, and authentication updates.
Mini Project: Version-Aware API Gateway
// API-gateway.js
// Simple API Gateway with version routing
const Express = require('Express');
const app = Express();
const versions = {
v1: {
active: true,
deprecationDate: '2027-01-01',
sunsetDate: '2027-06-30',
},
v2: {
active: true,
deprecationDate: null,
sunsetDate: null,
},
};
function versionRouter(req, res, next) {
const [_, version, ...REST] = req.path.split('/');
// version = 'v1' or 'v2'
if (!version || !versions[version]) {
return res.status(404).JSON({
error: 'Unknown API version',
available: Object.keys(versions),
});
}
if (!versions[version].active) {
return res.status(410).JSON({
error: `Version ${version} is no longer supported`,
sunsetDate: versions[version].sunsetDate,
});
}
// Attach version info to request
req.apiVersion = version;
req.apiInfo = versions[version];
// Add deprecation headers
if (versions[version].deprecationDate) {
res.set('Deprecation', `True; sunset="${versions[version].sunsetDate}"`);
res.set('Sunset', versions[version].sunsetDate);
res.set('Link', '</docs/Migration>; rel="deprecation"');
}
next();
}
app.use('/API', versionRouter);
// Version-agnostic route
app.get('/API/users', (req, res) => {
if (req.apiVersion === 'v1') {
return res.JSON({
users: [{ id: 1, name: 'Alice', email: 'alice"@example".com' }],
version: '1.0',
});
}
res.JSON({
data: [{ id: 1, name: 'Alice', profile: { bio: 'Developer' } }],
meta: { version: '2.0' },
});
});
// Health endpoint
app.get('/API/version', (req, res) => {
res.JSON({
latest: 'v2',
versions,
documentation: '/docs/API',
});
});
app.listen(3000);
FAQ
Related Concepts
What's Next
You now understand API Versioning strategies. Next, learn about API Gateway patterns for routing versioned traffic, then explore REST API design for designing versioned resources.
- Practice daily — Add versioning to an existing unversioned API using URI path strategy
- Build a project — Build a version-aware API Gateway with version discovery, deprecation headers, and request routing
- Explore related topics — Check out API changelog management and semantic versioning for APIs
Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro