Why Routing Belongs at the Gateway
Request routing is the core function of an API gateway. The gateway inspects each incoming request and determines which backend service should handle it. Centralizing routing in the gateway means that:
- Clients use a single stable hostname (
api.example.com) regardless of how many backend services exist or how they are reorganized - Service boundaries can change without requiring client updates
- Traffic can be split between service versions for canary deployments
- A new microservice can be introduced by simply adding a routing rule
Path-Based Routing
The most common routing strategy: match the URL path prefix to a backend service.
GET /api/v1/users/123 → users-service
GET /api/v1/orders/456 → orders-service
POST /api/v1/payments → payments-service
GET /api/v1/products/789 → catalog-service
Prefix Stripping
Gateways often strip the routing prefix before forwarding, so backend services receive clean paths without the /api/v1 namespace:
# Kong route configuration
routes:
- name: users-route
paths: ["/api/v1/users"]
strip_path: true # /api/v1/users/123 → /users/123 upstream
service: users-service
# Envoy route config with prefix rewrite
routes:
- match:
prefix: "/api/v1/users"
route:
cluster: users_service
prefix_rewrite: "/users" # strip /api/v1
Regex Matching
Use regex for more complex path patterns, such as routing by resource type regardless of the specific ID:
# AWS API Gateway resource path
paths:
/users/{userId}:
get:
x-amazon-apigateway-integration:
uri: http://users.internal/{userId}
requestParameters:
integration.request.path.userId: method.request.path.userId
Header-Based Routing
Header-based routing matches request headers to determine the backend. This enables versioning without changing URLs and multi-tenant routing.
API Version via Accept Header
Content negotiation-style versioning uses the Accept header:
GET /api/users
Accept: application/vnd.example.v2+json
→ routed to users-service-v2
GET /api/users
Accept: application/vnd.example.v1+json
→ routed to users-service-v1
# Envoy header-based routing
routes:
- match:
prefix: "/api/users"
headers:
- name: accept
string_match:
contains: "vnd.example.v2"
route:
cluster: users_service_v2
- match:
prefix: "/api/users"
route:
cluster: users_service_v1 # default
Tenant Routing via Custom Header
Multi-tenant SaaS applications often route to tenant-specific backends using a custom header or subdomain:
X-Tenant-ID: enterprise-corp
→ routed to dedicated enterprise-corp cluster
X-Tenant-ID: startup-inc
→ routed to shared tenant pool
Content-Based Routing
Content-based routing inspects the request body or operation to determine the backend. This is more expensive (requires body parsing) but necessary for some protocols.
GraphQL Operation Routing
Route GraphQL requests to different backends based on the operation type or name:
-- Kong Lua plugin: route GraphQL mutations to write cluster
local body = kong.request.get_body()
if body and body:find('mutation') then
kong.service.set_target('graphql-write.internal', 4000)
else
kong.service.set_target('graphql-read.internal', 4000)
end
gRPC Service and Method Routing
gRPC requests encode the service and method in the :path pseudo-header as /package.Service/Method. The gateway can route based on this:
# Envoy gRPC-specific routing
routes:
- match:
prefix: "/example.UserService/"
route:
cluster: user_service_grpc
- match:
prefix: "/example.OrderService/"
route:
cluster: order_service_grpc
Traffic Splitting
Traffic splitting sends a percentage of requests to one backend and the remainder to another. This is the mechanism for canary deployments and A/B testing.
Canary Deployment (Weight-Based)
Gradually shift traffic from the current version (v1) to the new version (v2):
# Envoy weighted cluster routing
routes:
- match:
prefix: "/api/"
route:
weighted_clusters:
clusters:
- name: api_v1
weight: 90
- name: api_v2
weight: 10 # canary: 10% of traffic
total_weight: 100
# Kong canary plugin
plugins:
- name: canary
config:
percentage: 10 # 10% to canary
upstream_host: api-v2.internal
upstream_port: 8080
Sticky Sessions for A/B Testing
For A/B tests, you want users consistently routed to the same variant. Use a hash of a stable identifier (user ID, session cookie) to assign users:
# Envoy hash-based routing (sticky variant assignment)
route:
weighted_clusters:
clusters:
- name: variant_a
weight: 50
- name: variant_b
weight: 50
hash_policy:
- header:
header_name: x-user-id # hash on user ID for sticky assignment
Feature Flag Routing
Route based on a feature flag header set by the client or a feature flag service:
X-Feature-Flag: new-checkout-flow
→ routed to checkout-v2 service
Summary
Path-based routing is the foundation — map URL prefixes to services with prefix stripping so backends receive clean paths. Add header-based routing for API versioning and multi-tenancy. Use content-based routing only when necessary (GraphQL, gRPC method routing) because it requires body parsing. Traffic splitting via weighted clusters is the safest way to roll out new service versions — start at 1-5%, monitor error rates, and promote gradually.