API Versioning at the Gateway
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
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