Design API gateways that handle routing, authentication, rate limiting, and observability without becoming a bottleneck or a single point of failure. Covers gateway architectures, protocol translation, request transformation, circuit breaking, and the deployment patterns that keep your gateway fast and reliable.
API gateways sit at the edge of a microservices architecture, routing, transforming, and securing requests across a fleet of services. They are essential when a client — mobile app, web browser, IoT device — must interact with multiple backend services, each with its own API contract, versioning, and deployment cadence. The gateway becomes the single entry point: a consistent, scalable, and observable front door. It matters most when services evolve independently, clients require unified access, and performance, security, and observability are non-negotiable.
When a client sends a request to https://api.example.com/v1/users/123, the gateway must determine which internal service handles it. The most common pattern is path-based routing: each service is mapped to a prefix path.
GET /v1/users/123 HTTP/1.1
Host: api.example.com
The gateway matches this path against a table of routes, each with a service URL and optional middleware. A route rule consists of a path pattern and a target service endpoint.
routes:
- path: /v1/users/*
service: users-service:8080
method: GET
- path: /v1/orders/*
service: orders-service:8080
method: POST
- path: /v1/products/{id}
service: products-service:8080
method: GET
headers:
X-Internal-Service: products
The gateway uses the path field as a prefix match. GET /v1/users/123 hits the first route. The path may contain variables like {id}. The gateway extracts these and injects them into the request as headers or a JSON body.
The key failure mode is route ambiguity: two routes match the same path, and the first one wins. If the gateway evaluates routes in order, a route for /v1/users can silently override /v1/users/{id}. To prevent this, use explicit priority or exact-match flags.
routes:
- path: /v1/users
service: users-service:8080
method: GET
priority: 10
- path: /v1/users/{id}
service: users-service:8080
method: GET
priority: 5
With priority, the gateway selects the route with the highest priority. If not, it uses the first matching route. The default behavior is often prefix-first, so the shorter path wins. This leads to subtle bugs: a GET /v1/users returns a list, but GET /v1/users/123 returns a single user — but if the first route is not updated, both endpoints serve the same data.
Clients often expect different request formats than services. The gateway transforms headers and query parameters to bridge the gap.
For example, a client sends Authorization: Bearer abc123 but the backend expects X-Auth-Token: abc123. The gateway can rewrite headers.
transform:
headers:
from:
Authorization: Bearer
to:
X-Auth-Token: "Bearer {Authorization}"
The gateway parses the Authorization header, extracts the token, and sets X-Auth-Token with the full value. It can also pass through or enrich existing headers.
transform:
query:
from:
filter: "status=active"
to:
q: "{filter}"
limit: 20
Given GET /v1/users?filter=status=active, the gateway produces GET /users?q=status=active&limit=20.
A common mistake is overwriting instead of merging. If the gateway applies from and to maps, it may overwrite existing query parameters. The solution is to specify a merge strategy.
transform:
query:
merge: "append"
from:
filter: "status=active"
to:
q: "{filter}"
page: 1
Now, GET /v1/users?page=3&filter=status=active becomes GET /v1/users?q=status=active&page=3&limit=20. The merge: append strategy preserves existing keys.
Failure occurs when a query parameter is missing or malformed. If Authorization: Bearer but no token, the gateway returns 401 Unauthorized with a X-Gateway-Error: Missing token in Authorization header. The error is logged and optionally exposed to the client.
When a client requests a complex resource, multiple services may need to be called. The gateway aggregates their responses into a single JSON document.
For example, a GET /v1/users/123 may require:
/users/123 → user data/orders?userId=123 → user’s recent orders/products?ids=45,67 → product details for those ordersThe gateway orchestrates these calls in parallel and combines the results.
aggregation:
request:
method: GET
path: /v1/users/{id}
services:
- name: user
url: users-service:8080
path: /users/{id}
parameters:
format: "json"
- name: orders
url: orders-service:8080
path: /orders
query:
userId: "{id}"
status: "completed"
- name: products
url: products-service:8080
path: /products
query:
ids: "{order.productIds}"
response:
template: |
{
"user": { "user" },
"orders": { "orders" },
"products": {
"byId": { "products" }
}
}
The gateway executes the three service calls concurrently. It collects the responses and applies the template to merge them. The template uses a simple key-value mapping where {key} refers to a result from a service.
The gateway may fail to aggregate if any service returns a 5xx error. The default is fail-fast: if any service fails, the entire aggregation fails and returns 503 Service Unavailable. But this can be tuned.
aggregation:
strategy: "merge"
retry: 2
timeout: 1500
With strategy: merge, the gateway returns a partial response even if some services fail. retry specifies how many times to retry each service. timeout applies to the entire aggregation process.
A subtle issue: nested aggregation. If products calls /products/45, and that service returns id: 45, name: "Laptop", tags: ["electronics", "high-end"], the gateway can use the tags to enrich the user object.
template: |
{
"user": { "user" },
"orders": { "orders" },
"tags": { "products.tags" },
"totalOrders": { "orders.length" }
}
The gateway evaluates the template expression using a simple, nested key-path syntax. It can also apply transforms to aggregated data.
The gateway handles authentication and authorizes every request before forwarding it to services. It supports multiple schemes: Bearer tokens, API keys, JWTs, and OAuth.
For Bearer tokens:
auth:
scheme: "bearer"
header: "Authorization"
jwt:
issuer: "https://auth.example.com"
audience: "api.example.com"
requiredClaims:
- "email"
- "role"
policies:
- name: "admin-only"
condition: "user.role == 'admin'"
status: 403
message: "Only administrators can access this endpoint"
The gateway validates the JWT token: checks signature, expiration, issuer, and audience. It also evaluates policies, which are rules applied after token validation.
If a request lacks an Authorization header, the gateway returns 401 Unauthorized with a WWW-Authenticate: Bearer header.
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer error="invalid_token", error_description="Token expired"
Content-Type: application/json
{
"error": "invalid_token",
"error_description": "Token expired"
}
A common failure is token propagation: the gateway extracts claims from the JWT and forwards them as headers to the downstream services.
auth:
forwardClaims: true
claimHeaders:
- "email"
- "role"
- "sub"
Now, GET /v1/users/123 with a JWT containing email: [email protected] results in:
GET /users/123 HTTP/1.1
Host: users-service
X-User-Email: [email protected]
X-User-Role: admin
X-User-Sub: 987654321
Authorization: Bearer abc123
The downstream services do not need to validate tokens — they use the forwarded claims. This reduces load and improves latency.
Another failure mode: token caching and refresh. The gateway caches validated JWTs to avoid repeated signature checks. If the cache misses, it fetches the JWKs from the issuer’s .well-known/jwks.json endpoint. If the token is near expiry, the gateway can pre-fetch the updated token in the background.
The gateway standardizes error responses across services. It maps internal 5xx errors to a consistent client-facing format.
error:
status: 500
template: |
{
"error": {
"code": "{status}",
"message": "{message}",
"timestamp": "{now}",
"requestId": "{request.id}"
},
"details": {
"gateway": {
"service": "{service.name}",
"path": "{request.path}"
}
}
}
When a service returns 503 Service Unavailable, the gateway transforms it into:
{
"error": {
"code": "503",
"message": "Service Unavailable",
"timestamp": "2024-05-10T12:34:56Z",
"requestId": "a1b2c3d4"
},
"details": {
"gateway": {
"service": "orders-service",
"path": "/orders"
}
}
}
The gateway also handles error fallback. If a service fails, the gateway can serve a cached response or return a synthetic one.
error:
fallback:
strategy: "cache"
cacheTtl: 300
template: |
{
"error": {
"code": "500",
"message": "Service temporarily unavailable"
},
"data": {
"cached": true
}
}
This is critical for high-availability systems. The gateway can also log errors and send alerts when failure rates exceed thresholds.
The gateway is a natural place for request tracing, metrics, and logging.
observability:
tracing:
enabled: true
format: "w3c"
header: "traceparent"
metrics:
enabled: true
endpoint: "/metrics"
labels:
gateway: "api.example.com"
environment: "production"
logging:
format: "json"
level: "info"
fields:
- "request.id"
- "user.id"
- "service.name"
The gateway emits structured logs with fields from the request and service context. It can also inject trace IDs using the W3C Trace Context format.
GET /v1/users/123 HTTP/1.1
traceparent: 00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01
Each service in the chain can propagate this header, creating a full trace across services.
The gateway also exposes /metrics endpoint that returns Prometheus-formatted metrics:
# TYPE gateway_requests_total counter
gateway_requests_total{method="GET",path="/v1/users",status="200"} 1234
gateway_requests_total{method="POST",path="/v1/orders",status="503"} 56
The gateway can also auto-configure its own health check.
health:
path: "/health"
interval: 10s
timeout: 5s
status: 200
body: |
{
"status": "ok",
"uptime": "34567s"
}
When the gateway is down, it returns 503 Service Unavailable with a detailed message and a Retry-After header.
The gateway is the central nervous system of the microservices ecosystem: it routes, transforms, aggregates, authenticates, and observes. It is the single point of truth for client access — and its failure cascades across the entire system.
This page was rewritten on 18 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.
We use cookies for analytics (Google Analytics) and advertising (Google AdSense) to improve your experience and support free content. Privacy Policy