Skip to content

Request Transformation at the API Gateway

DodaTech Updated 2026-06-28 5 min read

In this tutorial, you'll learn about Request Transformation. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.

Request transformation modifies incoming requests before forwarding them to backend services, enabling protocol adaptation, header injection, and payload normalization.

What You'll Learn

By the end of this lesson, you will implement header modifications, body transformations, query parameter manipulation, and request enrichment patterns.

Why It Matters

Backend services cannot always be modified. Request transformation bridges the gap between client expectations and service contracts without changing either.

Real-World Use

A legacy SOAP service expects XML, but clients send JSON. The gateway transforms JSON to XML, converts the content-type header, and forwards to the legacy service.

Transformation Pipeline

flowchart LR
    Client -->|JSON Payload| GW[Gateway]
    GW -->|Add Auth Header| GW
    GW -->|Convert to XML| GW
    GW -->|Forward| Backend[Backend]
    style GW fill:#22c55e,color:#fff

Header Transformation

# header_transform.py
from typing import Dict, Optional

class HeaderTransformer:
    def __init__(self):
        self.add_headers: Dict[str, str] = {}
        self.remove_headers: set = set()
        self.rename_headers: Dict[str, str] = {}

    def add(self, name: str, value: str):
        self.add_headers[name] = value

    def remove(self, name: str):
        self.remove_headers.add(name.lower())

    def rename(self, old_name: str, new_name: str):
        self.rename_headers[old_name.lower()] = new_name

    def transform(self, headers: Dict[str, str]) -> Dict[str, str]:
        result = {}

        for key, value in headers.items():
            key_lower = key.lower()
            if key_lower in self.remove_headers:
                continue
            if key_lower in self.rename_headers:
                result[self.rename_headers[key_lower]] = value
            else:
                result[key] = value

        result.update(self.add_headers)
        return result

transformer = HeaderTransformer()
transformer.add("X-Gateway", "gateway-v1")
transformer.add("X-Request-ID", "req-12345")
transformer.remove("x-internal-token")
transformer.rename("user-agent", "X-Original-UA")

incoming = {
    "Content-Type": "application/json",
    "Authorization": "Bearer token123",
    "X-Internal-Token": "supersecret",
    "User-Agent": "curl/8.0",
}

transformed = transformer.transform(incoming)
for key, value in sorted(transformed.items()):
    print(f"{key}: {value}")

Expected output:

Authorization: Bearer token123
Content-Type: application/json
X-Gateway: gateway-v1
X-Original-UA: curl/8.0
X-Request-ID: req-12345

Body Transformation

# body_transform.py
import json
from typing import Any, Dict, Optional

class BodyTransformer:
    def add_field(self, body: Dict, key: str, value: Any) -> Dict:
        body[key] = value
        return body

    def remove_field(self, body: Dict, key: str) -> Dict:
        body.pop(key, None)
        return body

    def rename_field(self, body: Dict, old: str, new: str) -> Dict:
        if old in body:
            body[new] = body.pop(old)
        return body

    def wrap_in_envelope(self, body: Dict) -> Dict:
        return {"data": body, "meta": {"transformed": True, "version": "1.0"}}

    def json_to_xml(self, body: Dict) -> str:
        xml_parts = ["<request>"]
        for key, value in body.items():
            xml_parts.append(f"  <{key}>{value}</{key}>")
        xml_parts.append("</request>")
        return "\n".join(xml_parts)

transformer = BodyTransformer()

payload = {"username": "alice", "password": "secret123", "internal_id": "12345"}
payload = transformer.remove_field(payload, "internal_id")
payload = transformer.add_field(payload, "source", "web")
payload = transformer.wrap_in_envelope(payload)

print(json.dumps(payload, indent=2))

Expected output:

{
  "data": {
    "username": "alice",
    "password": "secret123",
    "source": "web"
  },
  "meta": {
    "transformed": true,
    "version": "1.0"
  }
}

Query Parameter Transformation

# query_transform.py
from urllib.parse import urlencode, parse_qs
from typing import Dict, List, Optional

class QueryTransformer:
    def __init__(self):
        self.add_params: Dict[str, str] = {}
        self.remove_params: set = set()
        self.rename_params: Dict[str, str] = {}

    def add(self, key: str, value: str):
        self.add_params[key] = value

    def remove(self, key: str):
        self.remove_params.add(key)

    def rename(self, old: str, new: str):
        self.rename_params[old] = new

    def transform(self, query_string: str) -> str:
        params = parse_qs(query_string)
        result = {}

        for key, values in params.items():
            if key in self.remove_params:
                continue
            new_key = self.rename_params.get(key, key)
            result[new_key] = values[-1]

        result.update(self.add_params)
        return urlencode(result)

transformer = QueryTransformer()
transformer.add("api_key", "gw_key_123")
transformer.remove("debug")
transformer.rename("v", "version")

result = transformer.transform("page=1&limit=20&v=2&debug=true")
print(f"Original: page=1&limit=20&v=2&debug=true")
print(f"Transformed: {result}")

Expected output:

Original: page=1&limit=20&v=2&debug=true
Transformed: page=1&limit=20&version=2&api_key=gw_key_123

Common Mistakes

1. Modifying Body for GET Requests

GET requests should not have bodies. Only transform headers and query parameters for GET. Apply body transformations to POST/PUT/PATCH.

2. Breaking Content-Length

Transforming the body without updating Content-Length causes truncated or malformed requests. Always recalculate Content-Length.

3. Not Preserving Original Headers

Removing headers like Accept, Content-Type, or Authorization can break downstream services. Preserve or transform, never blindly delete.

4. Adding Sensitive Data to Headers

Injecting API keys or secrets into headers that get logged exposes credentials. Use secure header passing mechanisms.

5. Case Sensitivity Issues

HTTP headers are case-insensitive, but some frameworks treat them case-sensitively. Normalize header names to avoid conflicts.

Practice Questions

1. Why would you transform request headers at the gateway?

To inject authentication tokens, add tracing headers, remove internal headers before they reach clients, or rename headers for compatibility.

2. What is body enrichment?

Adding data to the request body that the client did not provide, such as client IP, timestamp, or user context from the authentication token.

3. How does request transformation support legacy systems?

It converts modern protocols (JSON/REST) to legacy formats (XML/SOAP) without modifying the legacy backend.

4. Why must Content-Length be recalculated after body transformation?

The body size changes after transformation, and an incorrect Content-Length causes the backend to misread the request.

Challenge

Design a transformation pipeline that converts a RESTful JSON request to a SOAP XML request, including header conversion, body restructuring, and content-type changes.

FAQ

Can transformation add latency?

Yes. Body transformations that parse and rebuild payloads add 5-50ms depending on payload size.

Should transformations be reversible?

Not always. Security transformations like removing sensitive fields should not be reversible.

Can I conditionally apply transformations?

Yes. Apply transformations based on route, header values, or client type for flexible processing.

How do I handle large payload transformations?

Stream the transformation process instead of loading the full body into memory for files over 10MB.

Is it better to transform at the gateway or service?

Gateway for protocol adaptation. Service for business logic. Keep business transformation in the service.

Mini Project: Request Transformer

# full_transformer.py
import json
from typing import Any, Dict, Optional

class RequestTransformer:
    def __init__(self):
        self.header_rules = {}
        self.body_rules = {}
        self.query_rules = {}

    def add_rule(self, stage: str, rule: Dict):
        if stage == "header":
            self.header_rules = rule
        elif stage == "body":
            self.body_rules = rule
        elif stage == "query":
            self.query_rules = rule

    def transform(self, request: Dict) -> Dict:
        result = dict(request)

        if "headers" in result and self.header_rules:
            for add_header in self.header_rules.get("add", []):
                result["headers"][add_header["name"]] = add_header["value"]

        if "body" in result and self.body_rules:
            for remove_field in self.body_rules.get("remove", []):
                result["body"].pop(remove_field, None)
            for add_field in self.body_rules.get("add", []):
                result["body"][add_field["name"]] = add_field["value"]

        return result

xf = RequestTransformer()
xf.add_rule("header", {"add": [{"name": "X-Gateway", "value": "gw-v1"}]})
xf.add_rule("body", {
    "remove": ["password", "ssn"],
    "add": [{"name": "processed_by", "value": "gateway"}],
})

req = {
    "method": "POST",
    "path": "/users",
    "headers": {"Content-Type": "application/json"},
    "body": {"username": "alice", "password": "secret", "ssn": "123-45-6789"},
}

transformed = xf.transform(req)
print(json.dumps(transformed, indent=2))

Expected output shows body without password/ssn, with processed_by field, and with X-Gateway header added.

What's Next

You understand request transformation. Next, learn about response transformation, then explore rate limiting at the gateway.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro