API Gateway

API Gateway is the front door for serverless applications, it handles routing, authentication, throttling, and CORS so your Lambda functions don't have to.

Introduction

API Gateway sits between your clients and your backend services. It receives HTTP requests, authenticates them, routes them to the right Lambda function (or any other integration), and returns the response. It handles millions of requests per second automatically, with built-in throttling, caching, and monitoring.

1. REST API vs. HTTP API

FeatureREST API (v1)HTTP API (v2)
Cost~$3.50/million requests~$1.00/million requests
LatencyHigherLower (~60% faster)
AuthorizersLambda, Cognito, IAMJWT (Cognito/Auth0), Lambda
Request/response mappingFull VTL templatesSimple only
Usage plans and API keysYesNo
WebSocket supportYesNo
Use forComplex requirements, third-party API monetizationMost applications - default choice

Default to HTTP API (v2) unless you specifically need usage plans, API keys, VTL request mapping, or WebSocket support. It is cheaper, faster, and has a cleaner CDK API.

2. Request Flow with JWT Authorizer

Authenticated API Request Flow

BrowserAPI GatewayCognitoLambdaDynamoDBPOST /orders (JWT)verify JWTclaimsevent + claimsPutItemok{ statusCode: 201 }201 Created

The JWT authorizer validates the token against Cognito before forwarding to Lambda

3. Lambda Proxy Integration

With Lambda proxy integration, API Gateway passes the full HTTP request to Lambda and expects a specific response format back. Your function has full control over status codes, headers, and body:

import json

# Lambda receives the full HTTP request as an event dict
def handler(event, context):
    method = event["requestContext"]["http"]["method"]
    path = event["requestContext"]["http"]["path"]
    headers = event.get("headers", {})
    body = event.get("body", "")   # string; parse JSON if needed
    params = event.get("queryStringParameters", {})

    auth_header = headers.get("authorization", "")
    # JWT payload is available after Cognito JWT authorizer validates it:
    user_id = event.get("requestContext", {}).get("authorizer", {}) \
                   .get("jwt", {}).get("claims", {}).get("sub", "")

    return {
        "statusCode": 200,
        "headers": {
            "Content-Type": "application/json",
            "Access-Control-Allow-Origin": "https://myapp.com",
        },
        "body": json.dumps({"userId": user_id, "path": path}),
    }

4. CORS, Stages, and Throttling

CORS is a browser security policy that blocks cross-origin requests unless the server explicitly allows them. API Gateway can handle CORS automatically for HTTP APIs. Set allow_origins to your frontend domain, not *, in production.

Stages represent deployment environments (dev, staging, prod). HTTP APIs create a $default stage automatically. REST APIs require explicit stage creation. Throttling protects your backend from traffic spikes:

# REST API - stages are explicit
# Create a "prod" stage
aws apigateway create-stage \
  --rest-api-id abc123 \
  --stage-name prod \
  --deployment-id xyz789 \
  --throttle '{"burstLimit": 500, "rateLimit": 1000}'

# Throttling: rateLimit = sustained requests/sec
#             burstLimit = token bucket size (spike capacity)

# HTTP API (v2) - $default stage is auto-created
# Custom domains point to the stage URL:
# https://api.myapp.com -> https://abc123.execute-api.us-east-1.amazonaws.com/prod

5. Deploying with CDK (Python)

The cleanest way to set up API Gateway is via CDK. The HTTP API v2 construct handles CORS, route definitions, and Lambda integration with minimal boilerplate:

import aws_cdk as cdk
from aws_cdk import aws_apigatewayv2 as apigwv2
from aws_cdk import aws_apigatewayv2_integrations as integrations
from aws_cdk import aws_lambda as lambda_

class ApiStack(cdk.Stack):
    def __init__(self, scope, id, **kwargs):
        super().__init__(scope, id, **kwargs)

        # The Lambda function
        fn = lambda_.Function(self, "Handler",
            runtime=lambda_.Runtime.PYTHON_3_12,
            handler="app.handler",
            code=lambda_.Code.from_asset("lambda"),
        )

        # HTTP API (v2) - cheaper and simpler than REST API (v1)
        api = apigwv2.HttpApi(self, "Api",
            cors_preflight=apigwv2.CorsPreflightOptions(
                allow_origins=["https://myapp.com"],
                allow_methods=[apigwv2.CorsHttpMethod.ANY],
                allow_headers=["content-type", "authorization"],
            ),
        )

        # Add a route: POST /orders -> Lambda
        api.add_routes(
            path="/orders",
            methods=[apigwv2.HttpMethod.POST],
            integration=integrations.HttpLambdaIntegration("OrderInt", fn),
        )

        cdk.CfnOutput(self, "ApiUrl", value=api.api_endpoint)

Key Takeaways

  • HTTP API (v2) is the default choice - cheaper, faster, and simpler than REST API (v1)
  • Lambda proxy integration passes the full HTTP request as an event; return a dict with statusCode, headers, and body
  • JWT authorizer validates tokens against Cognito (or Auth0) before forwarding; claims are available in the event
  • CORS - configure allowed origins explicitly on the API, not * in production
  • Throttling - set rate and burst limits to protect your Lambda from traffic spikes and unexpected cost
  • CDK is the cleanest way to define routes, integrations, and CORS in code rather than through the console