API Gateway Patterns: The Front Door to Your Microservices

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.

Request Routing and Path-Based Dispatching

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.

Header and Query Parameter Transformation

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.

Request and Response Aggregation

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:

The 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.

Authentication and Authorization at the Edge

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.

Error Handling and Default Response Shape

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.

Performance and Observability

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.