Request Routing at the API Gateway
In this tutorial, you'll learn about Request Routing. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.
Request routing is the process of directing incoming API requests to the appropriate backend service based on URL path, HTTP method, headers, or other request attributes.
What You'll Learn
By the end of this lesson, you will implement path-based, header-based, and method-based routing, understand service discovery integration, and configure dynamic routes.
Why It Matters
Routing is the core function of an API Gateway. Incorrect routing causes requests to reach the wrong service, resulting in errors, data corruption, or security breaches.
Real-World Use
Kong routes /users to the user service, /products to the catalog service, and /orders to the order service, each running on different clusters.
Routing Types
flowchart TD
Request[Incoming Request] --> Router{Router}
Router -->|path: /users/*| User[User Service]
Router -->|path: /products/*| Product[Product Service]
Router -->|header: X-Region: EU| EU[EU Cluster]
Router -->|method: POST| Create[Create Handler]
Router -->|default| Default[Default Service]
Path-Based Routing
The most common approach: route by URL path prefix.
# path_routing.py
from typing import Dict, Optional
class PathRouter:
def __init__(self):
self.routes: Dict[str, dict] = {}
def add_route(self, prefix: str, service: str, strip_prefix: bool = True):
self.routes[prefix] = {
"service": service,
"strip_prefix": strip_prefix,
}
def route(self, path: str) -> Optional[dict]:
sorted_prefixes = sorted(self.routes.keys(), key=len, reverse=True)
for prefix in sorted_prefixes:
if path.startswith(prefix):
route_config = self.routes[prefix]
remaining_path = path[len(prefix):] if route_config["strip_prefix"] else path
return {
"service": route_config["service"],
"path": remaining_path or "/",
"original_path": path,
}
return None
router = PathRouter()
router.add_route("/api/v1/users", "user-service:3000")
router.add_route("/api/v1/products", "product-service:4000")
router.add_route("/api/v1/orders", "order-service:5000")
paths = ["/api/v1/users/123", "/api/v1/products", "/api/v1/orders/new", "/api/v1/admin"]
for path in paths:
result = router.route(path)
if result:
print(f"{path:40s} -> {result['service']:25s} {result['path']}")
else:
print(f"{path:40s} -> NO ROUTE FOUND")
Expected output:
/api/v1/users/123 -> user-service:3000 /123
/api/v1/products -> product-service:4000 /
/api/v1/orders/new -> order-service:5000 /new
/api/v1/admin -> NO ROUTE FOUND
Header-Based Routing
Route based on request headers, useful for canary deployments and A/B testing.
# header_routing.py
import json
from typing import Dict, Optional
class HeaderRouter:
def __init__(self):
self.header_routes: Dict[str, Dict[str, str]] = {}
self.default_service: Optional[str] = None
def add_header_route(self, header: str, value: str, service: str):
if header not in self.header_routes:
self.header_routes[header] = {}
self.header_routes[header][value] = service
def set_default(self, service: str):
self.default_service = service
def route(self, headers: Dict[str, str]) -> Optional[str]:
for header_name, value_map in self.header_routes.items():
header_value = headers.get(header_name)
if header_value and header_value in value_map:
return value_map[header_value]
return self.default_service
router = HeaderRouter()
router.add_header_route("X-Canary", "v2", "user-service-v2:3000")
router.add_header_route("X-Region", "eu", "eu-cluster:3000")
router.add_header_route("X-Region", "us", "us-cluster:3000")
router.set_default("user-service-stable:3000")
test_headers = [
{"X-Canary": "v2"},
{"X-Region": "eu"},
{"X-Region": "us"},
{"Authorization": "Bearer token"},
]
for headers in test_headers:
service = router.route(headers)
print(f"Headers: {headers} -> {service}")
Expected output:
Headers: {'X-Canary': 'v2'} -> user-service-v2:3000
Headers: {'X-Region': 'eu'} -> eu-cluster:3000
Headers: {'X-Region': 'us'} -> us-cluster:3000
Headers: {'Authorization': 'Bearer token'} -> user-service-stable:3000
Method-Based Routing
# method_routing.py
from typing import Dict
class MethodRouter:
def __init__(self):
self.method_routes: Dict[str, str] = {}
def add_route(self, method: str, service: str):
self.method_routes[method.upper()] = service
def route(self, method: str, path: str) -> dict:
method = method.upper()
if method in self.method_routes:
return {
"service": self.method_routes[method],
"method": method,
"path": path,
}
return {"error": f"Method {method} not supported"}
router = MethodRouter()
router.add_route("GET", "query-service:3000")
router.add_route("POST", "command-service:3000")
router.add_route("DELETE", "admin-service:3000")
for method in ["GET", "POST", "DELETE", "PATCH"]:
result = router.route(method, "/api/resource")
print(f"{method:10s} -> {result.get('service', 'error')}")
Expected output:
GET -> query-service:3000
POST -> command-service:3000
DELETE -> admin-service:3000
PATCH -> error
Common Mistakes
1. Longest-Prefix Matching Order
Without sorting routes by length, a short prefix like /api can match before /api/v2. Always match most specific routes first.
2. Not Stripping Prefixes
When routing /api/v1/users to a service, the service receives /api/v1/users instead of /users. Strip the prefix before forwarding.
3. Hardcoding Service Addresses
Hardcoded addresses break when services scale or redeploy. Integrate with service discovery (Consul, Kubernetes DNS).
4. Case-Sensitive Routes
URL paths can be case-insensitive in some clients. Normalize paths to lowercase before matching.
5. Routing Based on Query Parameters Only
Query parameters can be lost or reordered. Use headers or path for critical routing decisions.
Practice Questions
1. What is the most common routing Strategy used in API gateways?
Path-based routing, where URL prefixes determine which service handles the request.
2. Why should you strip the route prefix before forwarding?
Backend services expect their native paths, not gateway-prefixed paths. Stripping keeps services decoupled from gateway configuration.
3. How does header-based routing support canary deployments?
By routing a subset of traffic (based on a header) to a new version while the rest stays on the stable version.
4. What is the role of service discovery in routing?
It provides the gateway with dynamic service locations so routes work even as services scale up and down.
Challenge
Design a routing configuration for a platform with four services, three deployment regions, and a canary flag, where traffic is routed based on path, region header, and canary header.
FAQ
Mini Project: Multi-Strategy Router
# multi_router.py
import json
from typing import Dict, List, Optional
class Route:
def __init__(self, paths: List[str], methods: List[str],
service: str, headers: Optional[Dict[str, str]] = None):
self.paths = paths
self.methods = [m.upper() for m in methods]
self.service = service
self.headers = headers or {}
class MultiStrategyRouter:
def __init__(self):
self.routes: List[Route] = []
def add_route(self, route: Route):
self.routes.append(route)
def route(self, method: str, path: str, headers: Dict[str, str]) -> dict:
method = method.upper()
sorted_routes = sorted(self.routes,
key=lambda r: max(len(p) for p in r.paths),
reverse=True)
for route in sorted_routes:
if method not in route.methods:
continue
if not any(path.startswith(p) for p in route.paths):
continue
if not all(headers.get(k) == v for k, v in route.headers.items()):
continue
return {"service": route.service, "matched": True}
return {"service": None, "matched": False}
router = MultiStrategyRouter()
router.add_route(Route(["/api/users"], ["GET", "POST"], "user-service"))
router.add_route(Route(["/api/orders"], ["GET"], "order-service", {"X-Region": "us"}))
router.add_route(Route(["/api/orders"], ["GET"], "order-service-eu", {"X-Region": "eu"}))
tests = [
("GET", "/api/users", {}),
("GET", "/api/orders", {"X-Region": "us"}),
("GET", "/api/orders", {"X-Region": "eu"}),
("POST", "/api/orders", {"X-Region": "us"}),
]
for method, path, headers in tests:
r = router.route(method, path, headers)
print(f"{method} {path} -> {r['service'] or 'NO MATCH'}")
Expected output:
GET /api/users -> user-service
GET /api/orders -> order-service
GET /api/orders -> order-service-eu
POST /api/orders -> NO MATCH
What's Next
You understand routing strategies. Next, learn about load balancing, then explore request transformation.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro