Siddhant Deval
Siddhant Deval
backend17 min read

API Gateway Essentials: Routing, Integrations & Authorization

Most teams choose REST API by default and wire Lambda Proxy Integration to every route — a decision that costs 70% more than HTTP API for identical use cases. This article covers the REST vs HTTP API cost model, the exact event shape Lambda Proxy Integration passes, Cognito User Pool vs Lambda Authorizer selection, and the Authorizer response caching mechanics that eliminate per-request auth Lambda invocations.

API Gateway Essentials: Routing, Integrations & Authorization

Every AWS primitive is a tradeoff surface, not a feature toggle. The AWS API Gateway console presents "REST API" as the first and most prominent option. Most teams choose it because tutorials choose it, and because it works. The mistake is not choosing REST API — it is choosing it without knowing that HTTP API provides identical Lambda Proxy Integration capability at 70% lower cost with lower base latency. This article closes the gap between "I wired Lambda to API Gateway" and "I understand what the gateway actually does with my request, what my Lambda must return, and how to configure authorization so it doesn't invoke a Lambda on every single API call."


1. REST API vs HTTP API — The $2,500-per-Billion-Requests Decision

API Gateway offers three API types. The naming is confusing — "REST API" and "HTTP API" both handle HTTP. The distinction is capability set and cost model.

1.1 Feature and Cost Comparison

Capability REST API HTTP API WebSocket API
Price per million requests $3.50 $1.00 $1.00 (connection) + $1.00 (messages)
Base latency ~10ms ~1ms N/A
Lambda Proxy Integration
JWT native authorizer
Lambda Authorizer
VTL mapping templates
Direct AWS service integration
Edge-optimized (CloudFront)
Usage plans / API keys
Private (VPC-only) endpoints
Response caching
Custom domain
CORS auto-config Partial N/A

The decision tree is straightforward:

Do you need VTL templates, direct AWS service integrations,
Edge-optimized (CloudFront-backed) endpoints, or usage plan / API key management?
  → YES: Use REST API. The capability premium is justified.
  → NO:  Use HTTP API. You are paying 70% more for nothing.
Crucial Requirement

At 1 billion requests/month, choosing REST API over HTTP API when HTTP API suffices costs $2,500 per month in excess API Gateway fees — before accounting for Lambda invocation costs. Audit existing REST APIs quarterly: if VTL, Edge endpoints, and usage plans are unused, migration to HTTP API is a zero-downside cost reduction.

1.2 Endpoint Types (REST API)

Type How it works Use case
Edge-optimized CloudFront distribution fronts the API; requests are routed to the nearest CloudFront PoP Global public APIs where geographic latency matters
Regional API endpoint lives in the deployed region; no CloudFront in the path APIs consumed from the same region (Lambda→Lambda, same-region clients)
Private Accessible only from a VPC via an Interface VPC Endpoint; no public internet routing Internal service-to-service APIs, compliance-required isolation

2. Lambda Proxy Integration — What Actually Happens

Lambda Proxy Integration is the default and most common integration type. It passes the entire HTTP request as a structured event object to Lambda and expects a structured response object back. Most engineers know this conceptually but have not inspected the actual shapes — leading to the most common API Gateway bug: a 502 produced by an incorrectly shaped Lambda response.

2.1 The Inbound Event Object

TYPESCRIPT
// The exact event shape for REST API Lambda Proxy Integration (APIGatewayProxyEvent)
export const handler = async (
  event: AWSLambda.APIGatewayProxyEvent
): Promise<AWSLambda.APIGatewayProxyResult> => {
  // Key fields your handler will actually use:
  const {
    httpMethod,           // "GET" | "POST" | "PUT" | "DELETE" | "PATCH"
    path,                 // "/users/42" (the actual URL path)
    pathParameters,       // { userId: "42" } (from {userId} in the route)
    queryStringParameters,// { page: "2", limit: "20" } (from ?page=2&limit=20)
    headers,              // { Authorization: "Bearer eyJ...", "Content-Type": "application/json" }
    body,                 // Raw string body (must be JSON.parsed if Content-Type is application/json)
    isBase64Encoded,      // true if binary body was base64-encoded by API GW
    requestContext: {
      requestId,          // API Gateway request ID — use as correlationId in logs
      authorizer: {
        claims,           // Populated by Cognito User Pool authorizer (JWT claims)
        principalId,      // Populated by Lambda Authorizer
        context,          // Custom key-value pairs returned by Lambda Authorizer
      }
    }
  } = event

  // ❌ Common mistake: accessing body without JSON.parse
  // event.body is ALWAYS a string in Lambda Proxy — never a parsed object
  const wrong = event.body.userId           // undefined — body is a string, not an object

  // ✅ Correct
  const payload = JSON.parse(event.body!)  // Parse the string body
  const { userId } = payload
}
Performance / Safety Warning

event.body is always a string | null in Lambda Proxy Integration — never a pre-parsed object. API Gateway passes the raw request body as a string regardless of Content-Type. Always JSON.parse(event.body) before accessing fields. TypeScript's type system will not catch this at compile time if you access event.body.field — it will silently return undefined.

2.2 The Required Response Shape

TYPESCRIPT
// ✅ Correct Lambda Proxy response — API Gateway reads these exact fields
return {
  statusCode: 200,           // Required: HTTP status code (number)
  headers: {                 // Optional but recommended: set Content-Type and CORS headers
    'Content-Type': 'application/json',
    'Access-Control-Allow-Origin': 'https://app.example.com',
  },
  body: JSON.stringify({     // Required: must be a STRING — not an object
    id: user.id,
    name: user.name,
  }),
  isBase64Encoded: false,    // Optional: set true only for binary responses
}

// ❌ These all produce 502 Bad Gateway from API Gateway:
return { id: '42', name: 'Alice' }           // Missing statusCode and body fields
return { statusCode: 200, body: { id: '42'}} // body must be string, not object
return 'Hello World'                          // Plain string — not the expected shape

The 502 produced by a malformed Lambda response is notoriously confusing because the error originates from API Gateway, not from Lambda. The Lambda logs show a successful invocation; only the API Gateway execution log (enable in Stage Settings) shows the malformed integration response.

Pro Tip & Optimization

Enable API Gateway execution logging (INFO level) on your stage during development. Without it, a 502 from a malformed Lambda response is invisible in Lambda logs and requires inference from CloudWatch metrics alone.

2.3 HTTP API Lambda Proxy (v2 payload format)

HTTP API uses payload format version 2.0, which has a cleaner shape:

TYPESCRIPT
export const handler = async (
  event: AWSLambda.APIGatewayProxyEventV2
): Promise<AWSLambda.APIGatewayProxyResultV2> => {
  // v2 differences from v1:
  const {
    routeKey,             // "GET /users/{userId}" — combines method + route
    rawPath,              // "/users/42"
    rawQueryString,       // "page=2&limit=20" (raw string, not parsed)
    pathParameters,       // { userId: "42" }
    body,                 // Still a string — still needs JSON.parse
    requestContext: {
      http: {
        method,           // "GET" — nested under requestContext.http in v2
        path,
        sourceIp,
      },
      requestId,
    }
  } = event

  // v2 allows shorthand response (statusCode defaults to 200):
  return JSON.stringify({ id: '42', name: 'Alice' }) // ← valid in v2: string response = 200
  // Or full form:
  return { statusCode: 200, body: JSON.stringify({ id: '42' }) }
}

3. Authorization: Cognito User Pool vs Lambda Authorizer

API Gateway supports multiple authorization mechanisms. The two most commonly confused are Cognito User Pool Authorizers and Lambda Authorizers — they solve different problems.

3.1 Cognito User Pool Authorizer

Cognito User Pool Authorizers validate JWTs issued by a Cognito User Pool without invoking any Lambda function. API Gateway calls the Cognito JWKS endpoint, validates the token signature and expiry, and injects the decoded JWT claims into event.requestContext.authorizer.claims.

TYPESCRIPT
// API Gateway Cognito authorizer configuration (CDK)
import { CognitoUserPoolsAuthorizer } from 'aws-cdk-lib/aws-apigateway'
import { UserPool } from 'aws-cdk-lib/aws-cognito'

const userPool = UserPool.fromUserPoolId(this, 'UserPool', 'us-east-1_abc123')

const authorizer = new CognitoUserPoolsAuthorizer(this, 'UserPoolAuthorizer', {
  cognitoUserPools: [userPool],
  identitySource: 'method.request.header.Authorization', // Bearer token location
})

// Apply to a route
api.root.addResource('orders').addMethod('GET', lambdaIntegration, {
  authorizer,
  authorizationType: AuthorizationType.COGNITO,
})
TYPESCRIPT
// Handler receives decoded Cognito claims — no token validation needed in handler
export const handler = async (event: AWSLambda.APIGatewayProxyEvent) => {
  const claims = event.requestContext.authorizer?.claims
  const userId = claims?.sub              // Cognito user UUID
  const email = claims?.email
  const groups = claims?.['cognito:groups']?.split(',') ?? []

  if (!groups.includes('admin')) {
    return { statusCode: 403, body: JSON.stringify({ error: 'Forbidden' }) }
  }
  // proceed...
}

When to use Cognito User Pool Authorizer:

  • Your application uses Cognito as its identity provider
  • You need JWT validation with no additional business logic
  • You want zero Lambda invocation cost for auth

3.2 Lambda Authorizer

Lambda Authorizers invoke a dedicated Lambda function for every authorization decision. The function receives the request context, validates the credential, and returns an IAM policy document.

TYPESCRIPT
// Lambda Authorizer handler (token-based)
export const authorizerHandler = async (
  event: AWSLambda.APIGatewayTokenAuthorizerEvent
): Promise<AWSLambda.APIGatewayAuthorizerResult> => {
  const token = event.authorizationToken.replace('Bearer ', '')

  try {
    const payload = await verifyJwt(token, process.env.JWKS_URL!)
    const userId = payload.sub

    // Return allow policy + context (passed to downstream Lambda in requestContext.authorizer)
    return {
      principalId: userId,
      policyDocument: {
        Version: '2012-10-17',
        Statement: [{
          Action: 'execute-api:Invoke',
          Effect: 'Allow',
          Resource: event.methodArn,  // Scope to this specific method
        }]
      },
      context: {
        userId,
        email: payload.email,
        tier: payload['custom:tier'] ?? 'free',
      }
    }
  } catch {
    throw new Error('Unauthorized') // API GW returns 401 on thrown Error
  }
}

3.3 Authorizer Response Caching — The Mandatory Configuration

Performance / Safety Warning

Lambda Authorizer caching is opt-in. The default TTL is 300 seconds, but you must explicitly set the ResultTtlInSeconds field. If you misconfigure it to 0, every API request invokes the Authorizer Lambda — at 10M requests/month, this adds 10M Authorizer invocations plus their latency contribution to every single API call.

TYPESCRIPT
// REST API: Lambda Authorizer with caching (CDK)
import { TokenAuthorizer } from 'aws-cdk-lib/aws-apigateway'

const authorizer = new TokenAuthorizer(this, 'TokenAuth', {
  handler: authorizerFn,
  resultsCacheTtl: Duration.seconds(300),   // Cache the policy for 5 minutes
  // Cache key: the token value by default (TOKEN authorizer)
  // For REQUEST authorizer: identitySource specifies which headers/params form the cache key
})

// Cache math:
// At 10M req/month with TTL=300s:
// Unique token active windows = 10M / (300s × assumed req rate) ≈ much lower
// Realistically: 99%+ cache hit rate for active user sessions
// Result: ~33K Authorizer invocations instead of 10M → 99.7% cost reduction

When to use Lambda Authorizer:

  • Third-party JWT issuer (Auth0, Okta, custom)
  • API key lookup against a DynamoDB or RDS table
  • Dynamic IAM policy generation based on request context
  • Request-parameter-based auth (checking headers, query parameters, not just bearer token)

4. Stages, Deployments & Stage Variables

API Gateway has a staging model that confuses many engineers: changes to your API configuration do not take effect until you create a Deployment and associate it with a Stage.

TYPESCRIPT
// CDK: explicit deployment with stage configuration
import { Deployment, Stage, LogGroupLogDestination, MethodLoggingLevel } from 'aws-cdk-lib/aws-apigateway'
import { LogGroup } from 'aws-cdk-lib/aws-logs'

const api = new RestApi(this, 'OrderApi', {
  deployOptions: {
    stageName: 'prod',
    loggingLevel: MethodLoggingLevel.INFO,  // Enable execution logging
    dataTraceEnabled: false,                 // Don't log full request/response bodies in prod
    metricsEnabled: true,
    throttlingBurstLimit: 500,
    throttlingRateLimit: 1000,
  }
})

// Stage variables: key-value pairs accessible in Lambda via event.stageVariables
// Useful for environment-specific routing without code changes
// stageVariables.lambdaAlias → "prod" or "staging" → used in Lambda ARN for alias routing
Pro Tip & Optimization

Use stage variables to route to Lambda aliases instead of hardcoded ARNs. arn:aws:lambda:us-east-1:123456:function:OrderProcessor:${stageVariables.lambdaAlias} lets you point prod stage at the prod Lambda alias and staging at the staging alias — enabling blue/green deployment at the API Gateway layer without changing infrastructure.


5. Throttling — Account, Stage, and Method Level

API Gateway throttling uses a token bucket algorithm at three levels:

Account limit: 10,000 req/s (default) — shared across all APIs in the region
  └── Stage limit: set per stage (overrides toward lower)
        └── Method limit: set per method (further overrides for specific routes)
TYPESCRIPT
// CDK: method-level throttling for a specific high-traffic route
const ordersResource = api.root.addResource('orders')
ordersResource.addMethod('POST', orderIntegration, {
  methodResponses: [{ statusCode: '200' }],
})

// Override throttling for the POST /orders method specifically
const deployment = new Deployment(this, 'Deployment', { api })
const stage = new Stage(this, 'ProdStage', {
  deployment,
  methodOptions: {
    'POST/orders': {
      throttlingBurstLimit: 100,   // Max burst: 100 simultaneous
      throttlingRateLimit: 50,     // Steady state: 50 req/s
    }
  }
})

Summary

Concept Rule
REST vs HTTP API HTTP API costs 70% less — use REST API only for VTL, Edge endpoints, or usage plans
Lambda Proxy Integration event.body is always a string; response must be { statusCode, body, headers } as exact shape
502 from API Gateway Produced by malformed Lambda response shape — enable execution logging to diagnose
Cognito User Pool Authorizer Validates JWTs without Lambda; use for Cognito-native auth
Lambda Authorizer Use for third-party tokens, API key lookup, dynamic policies
Authorizer caching TTL=0 invokes Authorizer on every request; set to 300s to reduce invocations by 99.7%

What's Next

In Part 4: API Gateway Advanced — VTL Templates, Direct Service Integrations & WAF, we move beyond Lambda Proxy to the integrations that eliminate Lambda from the hot path entirely: VTL mapping templates that transform HTTP requests directly into DynamoDB PutItem or SQS SendMessage operations, WebSocket connection ID tracking, and WAF rules that protect against rotating-IP abuse.

Research & Synthesis Note

This article was developed with AI-assisted deep search, specification cross-referencing, and technical research synthesis.

#API Gateway#AWS Lambda#REST API#HTTP API#Lambda Authorizer#Cognito#Serverless
Siddhant Deval

Written by Siddhant Deval

Senior Full-Stack Engineer building high-scale architectures, browser performance engineering systems, and SaaS platforms.