API Gateway Patterns

Request and Response Transformation at the API Gateway

How to transform requests and responses at the gateway — header injection, body modification, protocol translation (REST to gRPC), and response aggregation.

Why Transform at the Gateway?

Clients and backend services often speak slightly different dialects of the same protocol. A mobile client might expect camelCase JSON while your service produces snake_case. An external REST client might need to call a service that only speaks gRPC. A response might contain internal implementation details (database IDs, internal service names) that should be filtered before reaching the client.

The gateway is the ideal place to bridge these gaps. Transformation at the gateway means backends can be implemented in their natural form without concern for external API conventions, and clients get a consistent experience regardless of which backend their request is routed to.

Header Transformation

Header transformation is the most common and lightest-weight form of gateway transformation — it requires no body parsing.

Adding Correlation and Trace IDs

The gateway generates a unique request ID and injects it into every upstream request. This enables distributed tracing across services:

# Kong request-transformer plugin: add headers
plugins:
  - name: request-transformer
    config:
      add:
        headers:
          - "X-Request-ID: $(uuid)"
          - "X-Gateway-Version: 2.1"
          - "X-Forwarded-For: $(remote_addr)"
      remove:
        headers:
          - "X-Internal-Debug"
          - "X-Admin-Override"

Rewriting the Host Header

When forwarding to upstream services, the Host header must match what the upstream expects, not the gateway's hostname:

# Envoy: preserve original host or set upstream host
route:
  cluster: users_service
  host_rewrite_literal: "users.internal"  # sets Host for upstream
  # OR:
  auto_host_rewrite: true  # use cluster hostname

Response Header Removal

Strip internal headers from responses before they reach clients:

# Kong response-transformer: remove internal headers
plugins:
  - name: response-transformer
    config:
      remove:
        headers:
          - "X-Powered-By"        # don't reveal tech stack
          - "Server"              # don't reveal server software
          - "X-Internal-Node-ID"  # internal routing info

Body Transformation

Body transformation requires parsing the request or response payload — more expensive but often necessary to adapt external APIs to internal formats.

Field Renaming and Filtering

# Kong serverless plugin (Python) — transform response body
import json

def transform_response(body: dict) -> dict:
    return {
        'userId': body['user_id'],        # snake_case → camelCase
        'displayName': body['full_name'],  # rename field
        # Omit internal fields:
        # 'internal_db_id' → not included
        # 'shard_key' → not included
        'email': body['email'],
        'createdAt': body['created_at'],
    }

XML to JSON Conversion

Legacy SOAP backends can be exposed as REST APIs by having the gateway convert between XML and JSON:

# AWS API Gateway mapping template (VTL)
# Convert JSON request body → SOAP XML
#set($body = $input.json('$'))
<?xml version="1.0" encoding="UTF-8"?>
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
  <soapenv:Body>
    <GetUser>
      <UserId>$body.userId</UserId>
    </GetUser>
  </soapenv:Body>
</soapenv:Envelope>

Protocol Translation

Protocol translation converts between fundamentally different communication protocols. This is the most powerful — and most complex — form of gateway transformation.

REST to gRPC Transcoding

gRPC services can be exposed as REST APIs using the grpc-gateway project or Envoy's gRPC-JSON transcoder. The service's protobuf definition includes HTTP annotations that describe the REST mapping:

// users.proto — with HTTP annotations
import "google/api/annotations.proto";

service UserService {
  rpc GetUser(GetUserRequest) returns (User) {
    option (google.api.http) = {
      get: "/v1/users/{user_id}"  // GET /v1/users/123 → GetUser RPC
    };
  }
  rpc CreateUser(CreateUserRequest) returns (User) {
    option (google.api.http) = {
      post: "/v1/users"
      body: "*"  // entire JSON body maps to CreateUserRequest
    };
  }
}
# Envoy gRPC-JSON transcoder filter
http_filters:
  - name: envoy.filters.http.grpc_json_transcoder
    typed_config:
      proto_descriptor: /etc/envoy/users.pb  # compiled protobuf descriptor
      services: ["example.UserService"]
      print_options:
        add_whitespace: true
        always_print_primitive_fields: true

HTTP to WebSocket Upgrade

The gateway can upgrade HTTP connections to WebSocket connections transparently:

# Nginx WebSocket proxying
location /ws/ {
    proxy_pass http://websocket_backend;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_read_timeout 3600s;  # keep WS connection alive
}

Response Aggregation

The BFF (Backend-for-Frontend) pattern requires aggregating responses from multiple backend services into a single response for the client. This reduces the number of round trips the client must make:

# Gateway-level aggregation (Python serverless function)
import asyncio
import httpx

async def get_user_dashboard(user_id: str) -> dict:
    async with httpx.AsyncClient() as client:
        user_task = client.get(f'http://users.internal/users/{user_id}')
        orders_task = client.get(f'http://orders.internal/users/{user_id}/orders')
        prefs_task = client.get(f'http://prefs.internal/users/{user_id}/preferences')

        user, orders, prefs = await asyncio.gather(user_task, orders_task, prefs_task)

    return {
        'user': user.json(),
        'recent_orders': orders.json()['items'][:5],
        'preferences': prefs.json(),
    }

GraphQL as a Gateway

GraphQL federation (Apollo Federation, GraphQL Stitching) is another form of response aggregation — client queries are distributed to subgraph services, and the gateway merges the results into a single response graph.

Performance Considerations

Transformation adds latency. Minimize overhead:

  • Avoid body transformation for large payloads: streaming responses cannot be transformed without buffering the entire body
  • Use Lua or WASM over interpreted plugins: native code runs orders of magnitude faster than calling out to a sidecar process
  • Cache transformed responses: if the transformation is deterministic and the data infrequently changes, cache the transformed result
  • Measure before optimizing: add latency tracking before and after transformation filters to quantify the overhead

Summary

Header transformation is cheap and should be used liberally — add correlation IDs, strip internal headers, rewrite Host. Body transformation requires care — buffer size limits, streaming considerations, and performance overhead. Protocol translation (REST↔gRPC, HTTP↔WebSocket) is powerful but complex — use purpose-built tools like grpc-gateway or Envoy's transcoder filter rather than hand-rolling. Keep transformation logic simple; complex business logic belongs in services.

Protocoles associés

Termes du glossaire associés

Plus dans API Gateway Patterns