Skip to content

URI Path Versioning — Complete Guide

DodaTech Updated 2026-06-28 5 min read

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

Should I use /api/v1/users or /v1/users?

Both work. /api/v1 is more descriptive. Choose one and be consistent.

Can URI versioning work with CDN caching?

Yes. Each version URL is unique, making CDN caching straightforward.

How do I handle version redirects?

Use HTTP 301/302 redirects from unversioned URLs to the latest version, or return a version list.

Is URI versioning RESTful?

REST does not prescribe versioning. URI versioning is pragmatic and widely accepted.

Does URI versioning break hypermedia links?

Yes, if links are auto-generated without version context. Include version in generated links.

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