Skip to content

Versioning in REST APIs — Complete Guide

DodaTech Updated 2026-06-28 5 min read

In this tutorial, you'll learn about Versioning in REST. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.

REST API ilink "API Versioning" >}} manages changes to endpoints and responses over time without breaking existing clients, using strategies like URI path versioning, header versioning, and query parameter versioning.

What You'll Learn

By the end of this lesson, you will implement REST URI versioning, understand backward compatibility rules, and know how to deprecate and sunset old versions.

Why It Matters

REST APIs evolve. Without versioning, a breaking change affects every client simultaneously. Versioning gives clients control over when they upgrade.

Real-World Use

Twitter API v1.1 and v2 coexist. The v2 API has different endpoints and response formats. Clients opt into v2 explicitly via the URL path.

REST Versioning Strategies

flowchart LR
    URI[/v1/users] --> URIHandler
    URI[/v2/users] --> V2Handler
    Header["Accept: vnd.myapp.v2+json"] --> HeaderHandler
    Query["?version=2"] --> QueryHandler

URI Path Versioning

# uri_versioning.py
from typing import Any, Callable, Dict
import re

class URIVersionRouter:
    def __init__(self):
        self.v1_handlers: Dict[str, Callable] = {}
        self.v2_handlers: Dict[str, Callable] = {}

    def v1(self, path: str):
        def wrapper(fn):
            self.v1_handlers[path] = fn
            return fn
        return wrapper

    def v2(self, path: str):
        def wrapper(fn):
            self.v2_handlers[path] = fn
            return fn
        return wrapper

    def route(self, request_path: str) -> Dict:
        m = re.match(r"/v(\d+)(/.*)", request_path)
        if not m:
            return {"error": "No version in path"}

        version = int(m.group(1))
        path = m.group(2)

        handlers = {1: self.v1_handlers, 2: self.v2_handlers}
        handler = handlers.get(version, {}).get(path)

        if not handler:
            return {"error": f"v{version}{path} not found", "version": version}

        return handler()

router = URIVersionRouter()

@router.v1("/users")
def v1_users():
    return {"users": [{"id": 1, "name": "Alice"}], "version": "v1"}

@router.v2("/users")
def v2_users():
    return {"data": [{"id": 1, "name": "Alice", "email": "a@x.com"}],
            "meta": {"version": "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', 'email': 'a@x.com'}], 'meta': {'version': 'v2'}}
{'error': 'v3/users not found', 'version': 3}

Backward Compatibility Check

# rest_compat.py
from typing import Dict, List, Tuple

class BackwardCompatibility:
    def __init__(self, v1_schema: Dict, v2_schema: Dict):
        self.v1 = v1_schema
        self.v2 = v2_schema

    def check_fields(self) -> List[str]:
        issues = []
        v1_fields = set(self.v1.get("fields", []))
        v2_fields = set(self.v2.get("fields", []))

        removed = v1_fields - v2_fields
        for f in removed:
            issues.append(f"BREAKING: field '{f}' removed in v2")

        for f in v1_fields & v2_fields:
            v1_type = self.v1["types"].get(f)
            v2_type = self.v2["types"].get(f)
            if v1_type and v2_type and v1_type != v2_type:
                issues.append(f"BREAKING: field '{f}' type changed ({v1_type} -> {v2_type})")

        return issues

v1 = {"fields": ["id", "name", "email"], "types": {"id": "int", "name": "str", "email": "str"}}
v2 = {"fields": ["id", "name", "email"], "types": {"id": "int", "name": "str", "email": "str"}}

checker = BackwardCompatibility(v1, v2)
issues = checker.check_fields()
print(f"Issues: {issues if issues else 'None (backward compatible)'}")

v2_breaking = {"fields": ["id", "name"], "types": {"id": "int", "name": "str"}}
checker2 = BackwardCompatibility(v1, v2_breaking)
issues2 = checker2.check_fields()
print(f"Issues: {issues2}")

Expected output:

Issues: None (backward compatible)
Issues: ['BREAKING: field email removed in v2']

Deprecation Header Middleware

# rest_deprecation.py
from datetime import datetime, timedelta
from typing import Dict, Optional

class RESTDeprecation:
    def __init__(self):
        self.deprecated: Dict[str, datetime] = {}

    def deprecate(self, version: str, sunset_days: int = 180):
        self.deprecated[version] = datetime.utcnow() + timedelta(days=sunset_days)

    def headers(self, version: str) -> Dict[str, str]:
        sunset = self.deprecated.get(version)
        if not sunset:
            return {}
        return {
            "Deprecation": datetime.utcnow().strftime("%a, %d %b %Y %H:%M:%S GMT"),
            "Sunset": sunset.strftime("%a, %d %b %Y %H:%M:%S GMT"),
        }

dep = RESTDeprecation()
dep.deprecate("v1", sunset_days=180)

response_headers = dep.headers("v1")
print(f"v1 headers: {response_headers}")
print(f"v2 headers: {dep.headers('v2') or 'No deprecation'}")

Expected output:

v1 headers: {'Deprecation': '...', 'Sunset': '...'}
v2 headers: No deprecation

Common Mistakes

1. No Versioning at All

Public API with no versioning forces all clients to accept breaking changes simultaneously.

2. Breaking Changes in Patch Versions

Changing response fields without a new version. Add fields, do not remove or rename without a version bump.

3. Supporting Too Many Versions

Maintaining 5+ versions indefinitely creates maintenance burden. Support n-1 or n-2 at most.

4. No Deprecation Warning

Removing a version without warning. At minimum, add deprecation headers 6 months before removal.

5. Inconsistent Version Format

Using v1, v2 in URIs but different scheme in headers. Be consistent across the API.

Practice Questions

1. What is URI path versioning?

Including the version in the URL path: /v1/users, /v2/users.

2. How long should you support an old REST API version?

At minimum 6 months. 12 months is standard for public APIs.

3. What is a breaking change in REST?

Removing a field, changing a field type, removing an endpoint, changing authentication.

4. How do you communicate deprecation?

Deprecation HTTP header, Sunset header, documentation notices, and Migration guide.

Challenge

Build a REST versioning middleware that routes requests to the correct handler based on path version, adds deprecation headers for old versions, and logs version usage.

FAQ

Is URI versioning the best approach?

It is the most common because it is simple, visible, and cache-friendly. Each version has unique URLs.

Should I version my REST API from day one?

Yes. Even if you think the API is stable, version from v1. Adding versioning later is harder.

Can I use dates instead of version numbers?

Yes. Some APIs use dates (2024-01-01) instead of v1. Dates communicate when the version was released.

How do I handle internal vs external API versions?

Internal APIs can have different lifecycle. But standardize on versioning for both to avoid confusion.

Does versioning affect performance?

URI versioning has negligible overhead. Supporting multiple versions increases code complexity, not latency.

Mini Project: REST Version Manager

# rest_version_mgr.py
from typing import Any, Callable, Dict

class RESTVersionManager:
    def __init__(self):
        self.handlers: Dict[int, Dict[str, Callable]] = {}

    def register(self, version: int, path: str, handler: Callable):
        self.handlers.setdefault(version, {})[path] = handler

    def call(self, version: int, path: str) -> Dict:
        h = self.handlers.get(version, {}).get(path)
        return h() if h else {"error": f"v{version}{path} not found"}

mgr = RESTVersionManager()
mgr.register(1, "/users", lambda: {"users": []})
mgr.register(2, "/users", lambda: {"data": [], "meta": {"v": 2}})

print(mgr.call(1, "/users"))
print(mgr.call(2, "/users"))
print(mgr.call(3, "/users"))

Expected output:

{'users': []}
{'data': [], 'meta': {'v': 2}}
{'error': 'v3/users not found'}

What's Next

You understand REST versioning. Next, explore GraphQL versioning, then tooling and ecosystem comparison.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro