Steve Kinney

Request and Response Mapping

Say the Summit Supply frontend sends POST /items when someone adds a piece of gear to their saved list. The browser thinks in HTTP. Your Lambda function does not. API Gateway sits in the middle, turns that HTTP request into a JSON event object, hands it to Lambda, then turns Lambda’s return value back into an HTTP response on the way out. Understanding that translation layer is the difference between confidently building an API and guessing at field names until something works.

If you want AWS’s exact mapping syntax while you read, the HTTP API parameter mapping guide is the official reference.

Why This Matters

This is where frontend intuition meets AWS plumbing. If you understand the shape of the event object and the shape of the response object, API Gateway stops feeling magical and starts feeling mechanical. That is exactly what you want. Mechanical systems are easier to debug.

Builds On

sequenceDiagram
    participant Browser
    participant APIGateway as API Gateway
    participant Lambda

    Browser->>APIGateway: HTTP request
    APIGateway->>Lambda: APIGatewayProxyEventV2 JSON
    Lambda-->>APIGateway: { statusCode, headers, body }
    APIGateway-->>Browser: HTTP response

The Incoming Event: APIGatewayProxyEventV2

When you use an HTTP API with payload format version 2.0 (which you configured in Connecting API Gateway to Lambda), your handler receives an APIGatewayProxyEventV2 object. Here’s what a real event looks like for a POST /items?category=books request:

{
  "version": "2.0",
  "routeKey": "POST /items",
  "rawPath": "/items",
  "rawQueryString": "category=books",
  "headers": {
    "content-type": "application/json",
    "authorization": "Bearer eyJhbG...",
    "host": "abc123def4.execute-api.us-east-1.amazonaws.com",
    "user-agent": "Mozilla/5.0..."
  },
  "queryStringParameters": {
    "category": "books"
  },
  "requestContext": {
    "accountId": "123456789012",
    "apiId": "abc123def4",
    "domainName": "abc123def4.execute-api.us-east-1.amazonaws.com",
    "http": {
      "method": "POST",
      "path": "/items",
      "protocol": "HTTP/1.1",
      "sourceIp": "203.0.113.42",
      "userAgent": "Mozilla/5.0..."
    },
    "requestId": "abc123-request-id",
    "stage": "$default",
    "time": "18/Mar/2026:12:00:00 +0000",
    "timeEpoch": 1774051200000
  },
  "body": "{\"name\":\"TypeScript in Action\",\"price\":29.99}",
  "isBase64Encoded": false
}

That’s a lot of fields. Here are the ones you’ll use in almost every handler.

The Fields That Matter

HTTP Method and Path

const method = event.requestContext.http.method;
const path = event.requestContext.http.path;

The method and path are nested under requestContext.http, not at the top level. This catches people who are used to Express or Next.js API routes where req.method is a top-level property.

Query String Parameters

const category = event.queryStringParameters?.category;

queryStringParameters is an object mapping parameter names to values. If the request has no query string, the field is undefined—not an empty object. Always use optional chaining.

For repeated query parameters (like ?tag=js&tag=ts), API Gateway joins the values with a comma: "js,ts". If you need individual values, split on the comma.

Headers

const contentType = event.headers['content-type'];
const authorization = event.headers['authorization'];

All header names are lowercased. If the client sends Content-Type, you access it as event.headers['content-type']. This is consistent behavior from API Gateway—you don’t need to handle case variations.

Request Body

const body = event.body ? JSON.parse(event.body) : null;Note event.body is always a string. You must parse it yourself.

The body is always a string, even when the client sends Content-Type: application/json. API Gateway doesn’t parse it for you. If the request has no body (GET requests, for example), event.body is undefined.

Wrap JSON.parse in a try/catch. A client can send a malformed body that parses unsuccessfully, and an unhandled exception in your handler returns a 500 with no useful information to the caller.

let body: unknown = null;
try {
  body = event.body ? JSON.parse(event.body) : null;
} catch {
  return {
    statusCode: 400,
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ error: 'Invalid JSON in request body' }),
  };
}

Path Parameters

If your route includes path parameters (like GET /items/{id}), the values are available in event.pathParameters:

const itemId = event.pathParameters?.id;

Like queryStringParameters, the pathParameters field is undefined when no path parameters are defined on the route—not an empty object. (I wish it were an empty object, but here we are.)

The Stage

const stage = event.requestContext.stage;

For the $default stage, this value is "$default". If you create named stages (covered in API Gateway Stages and Custom Domains), the stage name appears here. This can be useful for conditional behavior—different database tables per environment, different log levels, and so on.

The Response Format

Your handler returns an object that API Gateway converts into an HTTP response. The format is straightforward:

return {
  statusCode: 200,
  headers: {
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ items: [] }),
};

Required Fields

  • statusCode: An integer HTTP status code. API Gateway passes this through to the client.
  • body: A string. If you’re returning JSON, you must JSON.stringify it. If you forget, the client receives [object Object].

Optional Fields

  • headers: An object of response headers. You should always set Content-Type. You can also set caching headers, custom headers, or CORS headers (though CORS is better handled at the API Gateway level—covered in API Gateway CORS Configuration).
  • isBase64Encoded: Set to true if the body is base64-encoded binary data (images, PDFs). Defaults to false.
  • cookies: An array of Set-Cookie header values. Using this field instead of setting cookies in the headers object ensures proper formatting when multiple cookies are set.
return {
  statusCode: 200,
  headers: {
    'Content-Type': 'application/json',
    'Cache-Control': 'max-age=300',
  },
  cookies: ['session=abc123; HttpOnly; Secure; SameSite=Strict'],
  body: JSON.stringify({ items: [] }),
};

A Complete Handler Pattern

Here’s a pattern that handles the common cases—routing by method, parsing the body, reading path parameters, and returning proper error responses:

import type { APIGatewayProxyHandlerV2 } from 'aws-lambda';

interface Item {
  id: string;
  name: string;
  price: number;
}

export const handler: APIGatewayProxyHandlerV2 = async (event) => {
  const method = event.requestContext.http.method;
  const path = event.requestContext.http.path;

  if (method === 'GET' && path === '/items') {
    const category = event.queryStringParameters?.category;
Note Use event.queryStringParameters for filtering, sorting, and pagination.    const items: Item[] = []; // Fetch from database in a real application

    return {
      statusCode: 200,
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ items }),
    };
  }

  if (method === 'POST' && path === '/items') {
    let body: { name: string; price: number };

    try {
      body = JSON.parse(event.body || '{}');
    } catch {
      return {
        statusCode: 400,
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ error: 'Invalid JSON' }),
      };
    }

    if (!body.name || typeof body.price !== 'number') {
      return {
        statusCode: 400,
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ error: 'Missing required fields: name, price' }),
      };
    }

    const item: Item = {
      id: crypto.randomUUID(),
      name: body.name,
      price: body.price,
    };

    return {
      statusCode: 201,
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(item),
    };
  }

  return {
    statusCode: 404,
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ error: 'Not found' }),
  };
};

crypto.randomUUID() is available natively in Node.js 20. No need to install the uuid package for basic ID generation.

Payload Format Version 1.0 vs. 2.0

If you’re reading tutorials or Stack Overflow answers, you might see event shapes that look different from what’s described here. That’s because API Gateway has two payload format versions:

FeatureVersion 1.0Version 2.0
Method locationevent.httpMethodevent.requestContext.http.method
Path locationevent.pathevent.requestContext.http.path
Query paramsevent.queryStringParametersevent.queryStringParameters
Multi-value query paramsevent.multiValueQueryStringParametersComma-separated in single field
Multi-value headersevent.multiValueHeadersComma-separated in single field
TypeScript typeAPIGatewayProxyEventAPIGatewayProxyEventV2

Version 2.0 is the default for HTTP APIs created with --payload-format-version 2.0. Version 1.0 is the format used by REST APIs. This course uses 2.0 exclusively.

Common Mistakes

Accessing event.httpMethod instead of event.requestContext.http.method. This is a version 1.0 field. In version 2.0, it doesn’t exist. If your code references event.httpMethod, you’re either using the wrong payload format or following a REST API tutorial.

Returning an object as the body. The body must be a string. Returning { items: [] } instead of JSON.stringify({ items: [] }) produces [object Object] in the response.

Not handling undefined fields. queryStringParameters, pathParameters, and body can all be undefined. TypeScript warns you about this if you’re using the correct types—pay attention to those warnings.

Your API works, but try calling it from a React app running on localhost:3000. The browser blocks the request with a CORS error. You’ve seen this before—and now you’re on the server side of the problem. The next lesson covers configuring CORS on your HTTP API so your frontend can actually call it.

Verification

Use one real request and one direct Lambda invocation to prove you understand the mapping:

curl -i https://YOUR_API_ID.execute-api.us-east-1.amazonaws.com/items

aws lambda invoke \
  --function-name my-frontend-app-api \
  --payload fileb://test-event.json \
  --region us-east-1 \
  --output json \
  response.json

You should be able to answer these without guessing:

  • Which field contains the HTTP method?
  • Which field contains the raw request body?
  • Which field contains the stage name?
  • Which part of your return value becomes the actual browser response body?

Common Failure Modes

  • Forgetting to parse event.body: API Gateway gives you a string, not an already-parsed object.
  • Reading the wrong path field: requestContext.http.path and rawPath are related, but not interchangeable in every debugging session.
  • Returning a plain object as body: if you skip JSON.stringify, the client gets garbage instead of JSON.
  • Assuming Express conventions: there is no req, no res, and no middleware stack quietly filling in missing pieces.

Last modified on .