Skip to content

API Versioning at the Gateway

DodaTech Updated 2026-06-28 5 min read

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

The gateway can handle API versioning by routing requests to different backend versions based on URL path, headers, or query parameters, decoupling clients from version changes.

What You'll Learn

By the end of this lesson, you will implement URL-based, header-based, and content-negotiation version routing at the gateway.

Why It Matters

Versioning at the gateway allows different clients to use different API versions simultaneously, enabling gradual Migration without breaking existing integrations.

Real-World Use

The gateway routes /v1/users to the legacy user service and /v2/users to the new service. Mobile app v1 uses v1 endpoints while app v2 uses v2 endpoints.

Version Routing Strategies

flowchart TD
    Request --> GW[Gateway]
    GW -->|/v1/*| V1Services[V1 Backend Services]
    GW -->|/v2/*| V2Services[V2 Backend Services]
    GW -->|Accept: vnd.api.v2| V2Services
    GW -->|?version=2| V2Services

URL-Based Version Routing

# url_version.py
from typing import Dict, Optional, Tuple

class URLVersionRouter:
    def __init__(self):
        self.routes: Dict[str, Dict[str, str]] = {}

    def add_version(self, version: str, service: str, service_path: str):
        if version not in self.routes:
            self.routes[version] = {}
        self.routes[version][service] = service_path

    def route(self, path: str) -> Tuple[Optional[str], Optional[str], str]:
        parts = path.strip("/").split("/")

        if len(parts) < 2:
            return None, None, path

        version = parts[0]
        service = parts[1] if len(parts) > 1 else ""

        if version in self.routes and service in self.routes[version]:
            remaining = "/" + "/".join(parts[2:]) if len(parts) > 2 else "/"
            return version, self.routes[version][service], remaining

        return None, None, path

router = URLVersionRouter()
router.add_version("v1", "users", "http://user-service-v1:3000")
router.add_version("v2", "users", "http://user-service-v2:3000")
router.add_version("v1", "orders", "http://order-service-v1:4000")

paths = ["/v1/users/123", "/v2/users/123", "/v1/orders", "/v3/products"]
for path in paths:
    version, service, remaining = router.route(path)
    if version:
        print(f"{path:25s} -> {version:3s} {service:35s} {remaining}")
    else:
        print(f"{path:25s} -> NO ROUTE")

Expected output:

/v1/users/123              -> v1  http://user-service-v1:3000  /123
/v2/users/123              -> v2  http://user-service-v2:3000  /123
/v1/orders                 -> v1  http://order-service-v1:4000  /
/v3/products               -> NO ROUTE

Header-Based Version Detection

# header_version.py
import re
from typing import Dict, Optional, Tuple

class HeaderVersionDetector:
    def __init__(self):
        self.version_patterns = {
            "accept": re.compile(r"application/vnd\.myapp\.v(\d+)\+json"),
            "custom": re.compile(r"version=(\d+)"),
        }

    def detect(self, headers: Dict[str, str], query: Dict[str, str]) -> Tuple[Optional[int], str]:
        accept = headers.get("Accept", "")
        accept_match = self.version_patterns["accept"].search(accept)
        if accept_match:
            return int(accept_match.group(1)), "accept-header"

        x_version = headers.get("X-API-Version")
        if x_version:
            try:
                return int(x_version), "x-api-version-header"
            except ValueError:
                pass

        version_param = query.get("version")
        if version_param:
            try:
                return int(version_param), "query-parameter"
            except ValueError:
                pass

        return None, "default"

detector = HeaderVersionDetector()

tests = [
    ({"Accept": "application/vnd.myapp.v2+json"}, {}, "Accept header v2"),
    ({"X-API-Version": "3"}, {}, "Custom header v3"),
    ({}, {"version": "1"}, "Query param v1"),
    ({}, {}, "No version info"),
]

for headers, query, desc in tests:
    version, source = detector.detect(headers, query)
    print(f"{desc:25s} -> version={version} (from {source})")

Expected output:

Accept header v2           -> version=2 (from accept-header)
Custom header v3           -> version=3 (from x-api-version-header)
Query param v1             -> version=1 (from query-parameter)
No version info            -> version=None (from default)

Common Mistakes

1. Hardcoding Version URLs In Clients

Clients should discover version URLs from a version endpoint. Hardcoded URLs make migration painful.

2. Not Deprecating Old Versions

Old versions accumulate Technical Debt. Have a deprecation policy and sunset old versions on schedule.

3. Inconsistent Version Format

Using v1, v2, v3 is clear. Mixing v1, 2.0, version-three creates confusion. Choose one format and stick with it.

4. Breaking Changes in Minor Versions

Semantic versioning means major versions for breaking changes. Minor and patch versions must be backward compatible.

5. No Fallback for Unknown Versions

Return a clear error when an unsupported version is requested, including the list of supported versions.

Practice Questions

1. What are the three main versioning strategies at the gateway?

URL path versioning (/v1/), header-based versioning (Accept header), and query parameter versioning (?version=1).

2. Why is URL versioning the most common?

It is explicit, easy to test, cacheable at the CDN level, and visible in logs and monitoring.

3. How does the gateway handle version routing?

It detects the version from URL/headers/query, then routes the request to the appropriate backend service version.

4. What is the role of a version discovery endpoint?

It tells clients which versions are available, which are deprecated, and which is the recommended version.

Challenge

Design a version routing system that supports URL-based versioning for primary routing, header-based for canary testing, and includes a version discovery endpoint with deprecation information.

FAQ

Should I route versioning at the gateway or DNS level?

Gateway routing is more flexible. DNS versioning (v1.api.example.com) requires separate deployments and certificates.

How many versions should I support simultaneously?

2-3 versions maximum. Each additional version increases maintenance cost and testing surface.

Can I use the gateway to transform between versions?

Yes. The gateway can transform v1 requests to v2 format and translate responses back to v1 format for legacy clients.

How does versioning affect caching?

Different versions should have different cache keys. Include the version in the cache key to prevent mixing.

What is the sunset period for old versions?

Industry standard is 6-12 months for internal APIs, 12-24 months for public APIs with communicated timelines.

Mini Project: Version-Aware Gateway

# version_gateway.py
from typing import Dict, Optional, Tuple

class VersionAwareGateway:
    def __init__(self):
        self.versioned_backends = {}

    def add_backend(self, version: str, service: str, url: str):
        if version not in self.versioned_backends:
            self.versioned_backends[version] = {}
        self.versioned_backends[version][service] = url

    def route(self, method: str, path: str, headers: Dict) -> dict:
        version, service_path = self._detect_version(path, headers)
        if not version:
            return {"status": 400, "body": {"error": "Unknown API version"}}

        remaining_path = self._extract_path(path, version)
        service = service_path.split("/")[0] if service_path else ""
        backend = self.versioned_backends.get(version, {}).get(service)

        if not backend:
            return {"status": 404, "body": {"error": f"No backend for {version}/{service}"}}

        return {
            "status": 200,
            "body": {
                "version": version,
                "backend": backend,
                "path": remaining_path,
                "method": method,
            }
        }

    def _detect_version(self, path: str, headers: Dict) -> Tuple[Optional[str], str]:
        parts = path.strip("/").split("/")
        if parts and parts[0].startswith("v") and parts[0][1:].isdigit():
            return parts[0], "/".join(parts[1:])
        accept = headers.get("Accept", "")
        if "vnd.myapp.v2" in accept:
            return "v2", ""
        return None, ""

    def _extract_path(self, path: str, version: str) -> str:
        prefix = f"/{version}"
        if path.startswith(prefix):
            return path[len(prefix):] or "/"
        return path

gw = VersionAwareGateway()
gw.add_backend("v1", "users", "http://users-v1:3000")
gw.add_backend("v2", "users", "http://users-v2:3000")

tests = [
    ("GET", "/v1/users/123", {}),
    ("GET", "/v2/users/456", {}),
    ("GET", "/api/users", {"Accept": "application/vnd.myapp.v2+json"}),
]
for method, path, headers in tests:
    r = gw.route(method, path, headers)
    print(f"{method} {path:25s} -> {r['status']} version={r['body'].get('version', 'N/A')}")

Expected output:

GET /v1/users/123              -> 200 version=v1
GET /v2/users/456              -> 200 version=v2
GET /api/users                 -> 200 version=v2

What's Next

You understand versioning at the gateway. Next, learn about WebSocket gateway, then explore Kong API gateway.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro