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
| Feature | REST API (v1) | HTTP API (v2) |
|---|---|---|
| Cost | ~$3.50/million requests | ~$1.00/million requests |
| Latency | Higher | Lower (~60% faster) |
| Authorizers | Lambda, Cognito, IAM | JWT (Cognito/Auth0), Lambda |
| Request/response mapping | Full VTL templates | Simple only |
| Usage plans and API keys | Yes | No |
| WebSocket support | Yes | No |
| Use for | Complex requirements, third-party API monetization | Most 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
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/prod5. 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