URI Path Versioning — Complete Guide
In this tutorial, you'll learn about URI Path Versioning. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.
URI path versioning embeds the version number directly in the URL path, such as /v1/users or /api/v2/products, making the version explicit and visible.
What You'll Learn
By the end of this lesson, you will implement URI path versioning in Python and Node.js, understand the pros and cons, and know when to use this approach.
Why It Matters
URI path versioning is the most common and simplest versioning Strategy. Understanding it is essential because most public APIs use this approach.
Real-World Use
Twitter API uses /1.1/statuses, Stripe uses /v1/charges, and GitHub uses /api/v3/ in their URL paths.
URI Versioning Architecture
flowchart LR
Request --> Router{URL Router}
Router -->|/v1/*| V1[V1 Handlers]
Router -->|/v2/*| V2[V2 Handlers]
Router -->|default| Latest[Latest Version]
Flask URI Versioning
# uri_version_flask.py
from flask import Flask, jsonify, Blueprint
from typing import Dict, List
app = Flask(__name__)
# V1 Blueprint
v1 = Blueprint('v1', __name__, url_prefix='/api/v1')
users_db_v1 = [
{'id': 1, 'name': 'Alice', 'email': 'alice@example.com'},
{'id': 2, 'name': 'Bob', 'email': 'bob@example.com'},
]
@v1.route('/users')
def list_users_v1():
"""V1: Returns flat user list."""
return jsonify({'users': users_db_v1, 'version': '1.0'})
@v1.route('/users/<int:user_id>')
def get_user_v1(user_id):
user = next((u for u in users_db_v1 if u['id'] == user_id), None)
if not user:
return jsonify({'error': 'Not found'}), 404
return jsonify({'user': user, 'version': '1.0'})
# V2 Blueprint
v2 = Blueprint('v2', __name__, url_prefix='/api/v2')
users_db_v2 = [
{'id': 1, 'name': 'Alice', 'email': 'alice@example.com',
'profile': {'bio': 'Developer', 'avatar': '/avatars/a.jpg'}},
{'id': 2, 'name': 'Bob', 'email': 'bob@example.com',
'profile': {'bio': 'Designer', 'avatar': '/avatars/b.jpg'}},
]
@v2.route('/users')
def list_users_v2():
"""V2: Returns users with nested profile."""
return jsonify({
'data': users_db_v2,
'meta': {'version': '2.0', 'count': len(users_db_v2)},
})
@v2.route('/users/<int:user_id>')
def get_user_v2(user_id):
user = next((u for u in users_db_v2 if u['id'] == user_id), None)
if not user:
return jsonify({'error': {'code': 'NOT_FOUND', 'message': 'User not found'}}), 404
return jsonify({'data': user, 'meta': {'version': '2.0'}})
app.register_blueprint(v1)
app.register_blueprint(v2)
@app.route('/api/version')
def version_info():
return jsonify({
'versions': ['v1', 'v2'],
'latest': 'v2',
'deprecated': ['v1'],
})
if __name__ == '__main__':
print("Flask app with URI versioning configured")
print("Available endpoints:")
print(" GET /api/v1/users")
print(" GET /api/v1/users/<id>")
print(" GET /api/v2/users")
print(" GET /api/v2/users/<id>")
print(" GET /api/version")
Expected output:
Flask app with URI versioning configured
Available endpoints:
GET /api/v1/users
GET /api/v1/users/<id>
GET /api/v2/users
GET /api/v2/users/<id>
GET /api/version
Express URI Versioning
// uri_version_express.js
const express = require('express');
const app = express();
// V1 Router
const v1Router = express.Router();
const usersV1 = [
{ id: 1, name: 'Alice', email: 'alice@example.com' },
{ id: 2, name: 'Bob', email: 'bob@example.com' },
];
v1Router.get('/users', (req, res) => {
res.json({ users: usersV1, version: '1.0' });
});
v1Router.get('/users/:id', (req, res) => {
const user = usersV1.find(u => u.id === parseInt(req.params.id));
if (!user) return res.status(404).json({ error: 'Not found' });
res.json({ user, version: '1.0' });
});
// V2 Router
const v2Router = express.Router();
const usersV2 = [
{ id: 1, name: 'Alice', email: 'alice@example.com',
profile: { bio: 'Developer' } },
{ id: 2, name: 'Bob', email: 'bob@example.com',
profile: { bio: 'Designer' } },
];
v2Router.get('/users', (req, res) => {
res.json({ data: usersV2, meta: { version: '2.0', count: usersV2.length } });
});
v2Router.get('/users/:id', (req, res) => {
const user = usersV2.find(u => u.id === parseInt(req.params.id));
if (!user) return res.status(404).json({ error: { code: 'NOT_FOUND' } });
res.json({ data: user, meta: { version: '2.0' } });
});
app.use('/api/v1', v1Router);
app.use('/api/v2', v2Router);
app.listen(3000, () => {
console.log('Express URI versioning API running on port 3000');
});
Pros and Cons
| Aspect | URI Versioning |
|---|---|
| Simplicity | Very simple to implement and understand |
| Cacheability | Each version has unique URLs, great for Caching |
| Testability | Easy to test specific versions with curl |
| Visibility | Version is visible in every log, metric, and trace |
| URL Pollution | URLs become longer and less clean |
| Redirection | Moving to new version requires URL changes |
| SEO | Multiple URLs for same content (mitigate with canonical) |
Common Mistakes
1. Not Stripping the Version Prefix
When forwarding to backend services, the version prefix should be stripped so services do not need to know about versioning.
2. Inconsistent Version Format
Use consistent format: v1, v2, v3. Avoid mixing v1, v1.0, 1.0.
3. No Default Version Redirect
Requests to /api/users without version should either return the latest version or redirect to the latest version URL.
4. Deep Nesting of Version
/api/v1/users/123 is better than /v1/api/users/123 or /rest/api/v1/users/123. Keep prefixes minimal.
5. Breaking URI Structure Between Versions
If /v1/users returns a list and /v2/users returns an object, clients are confused. Keep resource structure consistent.
Practice Questions
1. What is URI path versioning?
Embedding the version in the URL path, such as /v1/users or /api/v2/products.
2. Why is URI versioning popular?
It is simple, explicit, cacheable, and easy to test with tools like curl.
3. What are the main drawbacks of URI versioning?
URLs become longer, and redirecting clients to new versions requires updating URLs in client code.
4. How should you handle the unversioned URL (/api/users)?
Redirect to the latest version or return a list of available versions with links.
Challenge
Implement a Flask or Express API with three versions (v1, v2, v3) for a products resource, where v1 returns flat data, v2 adds nested categories, and v3 adds pagination.
FAQ
Mini Project: Version Router
# uri_version_router.py
from typing import Dict, List, Optional, Callable
class VersionRouter:
def __init__(self):
self.handlers: Dict[str, Dict[str, Callable]] = {}
def add_version(self, version: str):
self.handlers[version] = {}
def add_handler(self, version: str, path: str, handler: Callable):
self.handlers.setdefault(version, {})[path] = handler
def route(self, version: str, path: str, **kwargs) -> dict:
handler = self.handlers.get(version, {}).get(path)
if not handler:
return {"error": f"No handler for {version}{path}"}
return handler(**kwargs)
router = VersionRouter()
router.add_version("v1")
router.add_version("v2")
def list_users_v1():
return {"users": [{"id": 1, "name": "Alice"}], "version": "v1"}
def list_users_v2():
return {"data": [{"id": 1, "name": "Alice", "role": "admin"}], "meta": {"version": "v2"}}
router.add_handler("v1", "/users", list_users_v1)
router.add_handler("v2", "/users", list_users_v2)
print(router.route("v1", "/users"))
print(router.route("v2", "/users"))
print(router.route("v3", "/users"))
Expected output:
{'users': [{'id': 1, 'name': 'Alice'}], 'version': 'v1'}
{'data': [{'id': 1, 'name': 'Alice', 'role': 'admin'}], 'meta': {'version': 'v2'}}
{'error': 'No handler for v3/users'}
What's Next
You understand URI versioning. Next, learn about header-based versioning, then explore query parameter versioning.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro