Custom Header Versioning for APIs — X-API-Version and Beyond
In this tutorial, you'll learn about Custom Header. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.
Custom header versioning uses a dedicated HTTP header to communicate the API version, keeping URLs clean and allowing version information to propagate through the request chain.
What You'll Learn
By the end of this lesson, you will implement custom header versioning with X-API-Version, propagate versions to downstream services, configure header-based routing, and handle missing headers gracefully.
Why It Matters
Custom header versioning is simple to implement and keeps URLs clean, but requires clients to set headers correctly and careful Vary header configuration for Caching.
Real-World Use
Durga Antivirus Pro uses the X-API-Version header for its internal Microservices, allowing consistent version propagation through the service mesh.
Version Header Router
Route requests based on the X-API-Version header.
from typing import Dict, Optional, Callable, List
class VersionHeaderRouter:
def __init__(self, header_name: str = "X-API-Version",
default_version: str = "v1"):
self.header_name = header_name
self.default_version = default_version
self.handlers: Dict[str, Callable] = {}
self.supported: List[str] = []
def register(self, version: str,
handler: Callable):
self.handlers[version] = handler
if version not in self.supported:
self.supported.append(version)
def extract_version(self, headers: Dict) -> str:
version = headers.get(self.header_name)
if version:
version = str(version).strip()
if not version.startswith("v"):
version = f"v{version}"
if version in self.handlers:
return version
return self.default_version
def route(self, headers: Dict,
request: Dict) -> Dict:
version = self.extract_version(headers)
handler = self.handlers.get(version)
if not handler:
return {"status": 400,
"body": {"error": f"Handler not found for {version}"}}
response = handler(request)
response.setdefault("headers", {})
response["headers"][self.header_name] = version
response["headers"]["Vary"] = self.header_name
return response
def get_supported(self) -> List[str]:
return list(self.supported)
router = VersionHeaderRouter()
router.register("v1", lambda r: {"status": 200,
"body": {"data": "v1 response"}})
router.register("v2", lambda r: {"status": 200,
"body": {"data": "v2 response"}})
resp = router.route({"X-API-Version": "v2"}, {})
print(f"Version: {resp['headers']['X-API-Version']}")
Version Propagation
Propagate the API version to downstream services.
from typing import Dict, Optional
class VersionPropagator:
def __init__(self, header_name: str = "X-API-Version"):
self.header_name = header_name
def extract_from_request(self, headers: Dict) -> Optional[str]:
return headers.get(self.header_name)
def add_to_upstream(self, headers: Dict,
version: Optional[str] = None) -> Dict:
if version:
headers[self.header_name] = version
return headers
def transform_for_downstream(self, headers: Dict,
mapping: Dict[str, str]) -> Dict:
version = self.extract_from_request(headers)
if version and version in mapping:
headers[self.header_name] = mapping[version]
return headers
def strip_version(self, headers: Dict) -> Dict:
headers.pop(self.header_name, None)
return headers
propagator = VersionPropagator()
internal_headers = propagator.add_to_upstream(
{}, "v2"
)
print(f"Propagated: {internal_headers}")
stripped = propagator.strip_version({"X-API-Version": "v2"})
print(f"Stripped: {stripped}")
Header Validation
Validate version headers for correctness.
from typing import Dict, Optional, Tuple
class VersionHeaderValidator:
def __init__(self, supported_versions: set):
self.supported = supported_versions
self.min_version = min(supported_versions)
self.max_version = max(supported_versions)
def validate(self, headers: Dict,
header_name: str = "X-API-Version"
) -> Tuple[bool, Optional[str]]:
value = headers.get(header_name)
if not value:
return True, None
version = str(value).strip()
if not version.startswith("v"):
version = f"v{version}"
if version not in self.supported:
supported = ", ".join(sorted(self.supported))
return False, (
f"Version {version} not supported. "
f"Supported: {supported}"
)
return True, None
def get_latest(self) -> str:
return max(self.supported)
def get_earliest(self) -> str:
return min(self.supported)
validator = VersionHeaderValidator({"v1", "v2", "v3"})
valid, error = validator.validate({"X-API-Version": "v4"})
print(f"Valid: {valid}, error: {error}")
valid, error = validator.validate({"X-API-Version": "v2"})
print(f"Valid: {valid}")
Common Mistakes
Mistake 1: Not Setting Vary Header
Without Vary: X-API-Version, caches serve the wrong version. Always set the Vary header.
Mistake 2: Case-Sensitive Header Matching
HTTP headers are case-insensitive. Normalize header names to lowercase for matching.
Mistake 3: No Default Version
When the version header is missing, provide a documented default version or return an error.
Mistake 4: Propagating to External Clients
Version headers are for your API. Strip them before sending responses to external clients.
Mistake 5: Ignoring Header in Logs
Log the version header in access logs for debugging and usage analysis.
Practice Questions
- What is the advantage of custom header versioning over URI versioning?
- Why is the Vary header important for header versioning?
- How do you propagate version headers to downstream services?
- What should happen when the version header is missing?
- How do you validate version header values?
Challenge
Build a custom header versioning system that routes based on X-API-Version, propagates versions to downstream services, sets Vary header for caching, validates supported versions, and provides a default version fallback.
FAQ
Mini Project
Build a custom header versioning system that routes requests based on X-API-Version header, supports versions v1 through v3 with a default of v2, sets Vary: X-API-Version on responses, propagates the version to upstream services, and validates the header on each request.
What's Next
Learn about Accept Header Versioning for content negotiation approaches, or explore Media Type Versioning for custom media type patterns.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro