Skip to content

Versioning REST APIs — URL, Header, and Content Negotiation Strategies

DodaTech Updated 2026-06-28 00:00:00+00:00 5 min read

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

REST API versioning strategies determine how you communicate version information between client and server, with each approach offering different trade-offs for visibility, cacheability, and client effort.

What You'll Learn

By the end of this lesson, you will implement URI-based, header-based, and content negotiation versioning strategies, understand their trade-offs, and choose the right approach for your REST API.

Why It Matters

The versioning Strategy you choose affects how easily consumers can discover and switch versions, how caches handle your responses, and how your API documentation reads.

Real-World Use

Durga Antivirus Pro uses URI versioning (/api/v2/) for its public REST API because it is visible, easy to document, and works well with CDN Caching.

REST Versioning Strategies

flowchart TD
    REST[REST API Versioning]-->URI[URI Versioning]
    REST-->Header[Header Versioning]
    REST-->Content[Content Negotiation]
    URI-->Example[/api/v2/users]
    Header-->Example2[X-API-Version: 2]
    Content-->Example3[Accept: application/vnd.api.v2+json]

URI Versioning

Version in the URL path - the most common approach.

from typing import Dict, Optional, List
import re

class URIVersionRouter:
    def __init__(self):
        self.handlers: Dict[str, callable] = {}
        self.supported_versions: set = set()

    def register_version(self, version: str,
                         handler: callable):
        self.handlers[version] = handler
        self.supported_versions.add(version)

    def extract_version(self, path: str
                        ) -> Optional[str]:
        match = re.match(r"/api/v(\d+)/", path)
        if match:
            return f"v{match.group(1)}"
        return None

    def route(self, path: str, request: Dict) -> Dict:
        version = self.extract_version(path)
        if not version:
            return {"status": 400,
                    "body": {"error": "Version required"}}
        if version not in self.supported_versions:
            return {"status": 404,
                    "body": {"error": f"Version {version} not supported"}}
        handler = self.handlers[version]
        return handler(request)

    def get_supported_versions(self) -> List[str]:
        return sorted(self.supported_versions)

router = URIVersionRouter()
def v1_handler(req):
    return {"status": 200,
            "body": {"version": "v1", "data": req}}
def v2_handler(req):
    return {"status": 200,
            "body": {"version": "v2", "data": req}}
router.register_version("v1", v1_handler)
router.register_version("v2", v2_handler)
result = router.route("/api/v2/scan", {"method": "GET"})
print(f"Routed: {result['body']['version']}")

Header Versioning

Version specified in a custom HTTP header.

from typing import Dict, Optional

class HeaderVersionRouter:
    def __init__(self, header_name: str = "X-API-Version"):
        self.header_name = header_name
        self.handlers: Dict[str, callable] = {}

    def register_version(self, version: str,
                         handler: callable):
        self.handlers[version] = handler

    def extract_version(self, headers: Dict
                        ) -> Optional[str]:
        version = headers.get(self.header_name)
        if version:
            if str(version).startswith("v"):
                return version
            return f"v{version}"
        return None

    def route(self, headers: Dict, request: Dict) -> Dict:
        version = self.extract_version(headers)
        if not version:
            version = "v1"
        handler = self.handlers.get(version)
        if not handler:
            return {"status": 400,
                    "body": {"error": f"Unsupported version: {version}"}}
        return handler(request)

    def get_version_header(self) -> str:
        return self.header_name

hrouter = HeaderVersionRouter()
hrouter.register_version("v1", lambda r: {"status": 200,
    "body": {"version": "v1"}})
hrouter.register_version("v2", lambda r: {"status": 200,
    "body": {"version": "v2"}})
result = hrouter.route({"X-API-Version": "v2"}, {})
print(f"Header routed: {result['body']['version']}")

Content Negotiation (Media Type) Versioning

Version in the Accept header using custom media types.

from typing import Dict, Optional, List
import re

class MediaTypeVersionRouter:
    def __init__(self):
        self.handlers: Dict[str, callable] = {}
        self.vendor_prefix = "vnd.api"

    def register_version(self, version: str,
                         handler: callable):
        self.handlers[version] = handler

    def extract_version(self, accept: str) -> Optional[str]:
        pattern = (
            rf"application/{self.vendor_prefix}\.v(\d+)\+json"
        )
        match = re.search(pattern, accept)
        if match:
            return f"v{match.group(1)}"
        return None

    def route(self, request: Dict) -> Dict:
        accept = request.get("headers", {}).get(
            "Accept", "application/json"
        )
        version = self.extract_version(accept)
        if not version:
            version = "v1"
        handler = self.handlers.get(version)
        if not handler:
            return {"status": 406,
                    "body": {"error": f"Version {version} not acceptable"}}
        return handler(request)

    def generate_accept_header(self, version: str) -> str:
        return f"application/{self.vendor_prefix}.{version}+json"

mt_router = MediaTypeVersionRouter()
mt_router.register_version("v1", lambda r: {"status": 200,
    "body": {"version": "v1", "format": "media-type"}})
mt_router.register_version("v2", lambda r: {"status": 200,
    "body": {"version": "v2", "format": "media-type"}})
result = mt_router.route({
    "headers": {
        "Accept": "application/vnd.api.v2+json"
    }
})
print(f"Media type routed: {result['body']['version']}")

Common Mistakes

Mistake 1: Inconsistent Version Placement

Using URI versioning for some endpoints and header versioning for others confuses consumers.

Mistake 2: Version as Query Parameter

Query parameter versioning is non-standard, hard to cache, and easy to miss.

Mistake 3: Breaking Content Negotiation Contracts

Changing the media type format between versions without documentation breaks consumers.

Mistake 4: No Default Version

When version is missing, either reject the request or clearly document which default version is used.

Mistake 5: Versioning the Wrong Things

Version the API contract, not the implementation. Internal implementation details should not affect versioning.

Practice Questions

  1. What is the advantage of URI versioning over header versioning?
  2. When would you choose content negotiation versioning?
  3. How does versioning affect HTTP caching?
  4. What is the default version when none is specified?
  5. Can you mix versioning strategies?

Challenge

Build a REST API versioning system that supports URI versioning (/api/v1/), header versioning (X-API-Version), and content negotiation (Accept header), with a configurable default version and clear error messages for unsupported versions.

FAQ

Which REST versioning strategy is most common?

URI versioning is the most common because it is visible, easy to implement, works with any HTTP client, and supports CDN caching.

Does URI versioning violate REST principles?

Some argue that changing the URI for different versions breaks the uniform interface. In practice, URI versioning is the most pragmatic and widely used approach.

How does caching work with header versioning?

Header versioning requires Vary: X-API-Version to cache responses correctly. Without it, caches may serve wrong versions.

What is the difference between media type versioning and content negotiation?

Media type versioning is a form of content negotiation where the version is encoded in the Accept header as a custom media type.

Can I remove old versions from the URI?

Old URIs should continue working or return proper redirects. Removing old URIs without notice breaks bookmarks and integrations.

Mini Project

Build a REST API versioning router that supports all three strategies (URI, header, content negotiation) with a priority chain, configurable per-route version support, proper error responses for unsupported versions, and Vary header injection for cache correctness.

What's Next

Learn about Versioning GraphQL APIs for Graphql-specific versioning, or explore Versioning gRPC APIs for protobuf-based versioning.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro