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.