Skip to content

Custom Header Versioning for APIs — X-API-Version and Beyond

DodaTech Updated 2026-06-28 4 min read

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

  1. What is the advantage of custom header versioning over URI versioning?
  2. Why is the Vary header important for header versioning?
  3. How do you propagate version headers to downstream services?
  4. What should happen when the version header is missing?
  5. 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

What is the most common custom header for versioning?

X-API-Version is the most common. Some APIs also use Accept-Version, Api-Version, or custom headers like X-MyApp-Version.

Does header versioning work with all HTTP clients?

Most HTTP clients support custom headers. Browser fetch and axios require explicit header configuration, unlike URL path versioning.

How does header versioning affect API documentation?

Documentation must specify both the endpoint URL and the required version header. Tools like Swagger support header parameters.

Can I use multiple version headers?

Use one version header for consistency. Multiple headers confuse clients and complicate parsing logic.

How do I debug header versioning issues?

Log the version header in access logs, return the version in response headers for debugging, and include the version in error responses.

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