Versioning REST APIs — URL, Header, and Content Negotiation Strategies
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
- What is the advantage of URI versioning over header versioning?
- When would you choose content negotiation versioning?
- How does versioning affect HTTP caching?
- What is the default version when none is specified?
- 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
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