Transforming API Gateway responses with mapping templates is less about changing the data and more about changing how the API Gateway presents that data to the client.
Let’s see this in action. Imagine you have a backend Lambda function that returns data like this:
{
"statusCode": 200,
"headers": {
"Content-Type": "application/json"
},
"body": "{\"userId\": \"user-123\", \"orderHistory\": [{\"orderId\": \"order-abc\", \"amount\": 150.75}, {\"orderId\": \"order-def\", \"amount\": 75.20}]}"
}
This is a common pattern for Lambda integrations where the Lambda itself is responsible for constructing the full HTTP response. However, what if your client just wants a clean list of order IDs, not the entire Lambda response structure? You can use a mapping template to achieve this.
In your API Gateway method’s "Integration Response" section, you’ll configure a "Mapping Templates". For a 200 OK response, you’d add a new template, select application/json as the Content Type, and paste in something like this:
{
"orders": $util.parseJson($input.json('$.body'))
}
Here’s the breakdown:
$util.parseJson(...): This is a built-in Velocity Template Language (VTL) function that takes a JSON string and parses it into a JSON object.$input.json('$.body'): This accesses thebodyfield from the integration response (which is the JSON string returned by your Lambda). The$signifies a VTL variable, and.json('$.body')is a way to safely extract the value associated with the keybodyfrom the input JSON.
When a client calls your API, the integration response from Lambda will be passed to this mapping template. The template will take the body string, parse it into a JSON object, and then construct a new JSON object that looks like this:
{
"orders": [
{"orderId": "order-abc", "amount": 150.75},
{"orderId": "order-def", "amount": 75.20}
]
}
Notice how the statusCode, headers, and the outer body key from the Lambda’s response are gone. The client now receives a simplified JSON structure directly containing the parsed order data under a new orders key.
You can do much more than just parse and re-key. You can filter, transform, and even construct entirely new payloads. For example, if you only wanted the order IDs:
{
"orderIds": [
#foreach($order in $util.parseJson($input.json('$.body')))
"$order.orderId" #if($foreach.hasNext), #end
#end
]
}
This template iterates through the parsed body, extracts just the orderId for each item, and builds a new JSON array of strings.
The core mental model is that the mapping template sits between your backend integration (Lambda, HTTP, etc.) and the client calling your API. It intercepts the response from the backend before it’s sent to the client and reshapes it according to the VTL rules you define. This allows you to decouple your backend’s internal data structures from the API contract exposed to your consumers. You can change your Lambda’s output significantly without breaking existing API clients, as long as the mapping template remains compatible.
A common pitfall is forgetting that the body from a Lambda integration is often a stringified JSON. If you try to access properties of $input.json('$.body') directly without util.parseJson, you’ll get VTL errors because you’re treating a string as an object.
The next step is often to handle different status codes with different mapping templates, or to start using mapping templates for request transformation.