AWS Advanced #4: API Gateway + Lambda
To call functions from #3 Lambda Basics over HTTP, you need a front door. The AWS standard is API Gateway — a managed gateway that sits in front of Lambda (or ECS, EC2, external backends).
This post covers the two flavors (REST / HTTP), Lambda integration patterns, authorization options, stages and deploys, and usage plans — all in one go.
Where API Gateway fits #
client (browser, app)
│ HTTPS
▼
API Gateway
├─ TLS termination
├─ Routing (path → backend)
├─ Auth / authz
├─ Rate limiting / throttling
├─ Request transformation
└─ Caching
│
▼
backend (Lambda / ECS / external HTTP)The role overlaps with ALB (Intermediate #6) but with a different emphasis.
| ALB | API Gateway | |
|---|---|---|
| Pricing model | Hourly + LCU | Per request + data |
| Cost at zero traffic | Hourly (~$20/month) | 0 |
| Direct Lambda integration | Possible | Native |
| API key / Usage Plan | None | Yes |
| Caching | None | Yes (REST API) |
| Cognito integration | Separate | Natural |
| Where it shines | Always-on big ECS | Bursty / serverless / API key needed |
REST API vs HTTP API #
API Gateway has two flavors.
HTTP API (v2) — newer #
- 70% cheaper, faster
- Validates JWT tokens (Cognito / Auth0 / your own OIDC) directly
- Simple CORS setup
- Lambda proxy integration by default
- No WebSocket, no API key / Usage Plan, no request transformation / caching
The default candidate for new projects. Simple REST API + JWT is enough.
REST API (v1) — older + richer #
- Caching (per stage)
- Request / response mapping transformations
- Direct WAF integration
- API Key + Usage Plan
- WebSocket support
When you need complex transformation / usage segmentation / WebSocket.
How to choose #
Need WebSocket? ─Yes→ REST API (or WebSocket API)
│ No
▼
Need API Key / Usage Plan? ─Yes→ REST API
│ No
▼
Need complex transformations / caching? ─Yes→ REST API
│ No
▼
HTTP API (cheap + fast)This post goes with HTTP API, calling out REST-only features as we go.
First API — Hello, World #
Lambda function #
import json
def handler(event, context):
name = event.get("queryStringParameters", {}).get("name", "world")
return {
"statusCode": 200,
"headers": {"Content-Type": "application/json"},
"body": json.dumps({"message": f"Hello, {name}!"})
}This is what we covered in #3. Deploy as hello-fn.
Create HTTP API (console) #
Console → API Gateway → “Create API” → “Build” under HTTP API.
- Add integration: Lambda → region / function (
hello-fn) - API name:
hello-api - Routes:
GET /hello→hello-fn - Stage:
$default(auto-deploy) - Create
URL once done: https://abc123.execute-api.ap-northeast-2.amazonaws.com/hello?name=AWS
Via CLI #
# create
API_ID=$(aws apigatewayv2 create-api \
--name hello-api \
--protocol-type HTTP \
--target arn:aws:lambda:ap-northeast-2:123456789012:function:hello-fn \
--query ApiId --output text)
# permission for API Gateway to invoke the Lambda
aws lambda add-permission \
--function-name hello-fn \
--statement-id apigw-invoke \
--action lambda:InvokeFunction \
--principal apigateway.amazonaws.com \
--source-arn "arn:aws:execute-api:ap-northeast-2:123456789012:$API_ID/*/*"
echo "https://$API_ID.execute-api.ap-northeast-2.amazonaws.com/"The --target shortcut creates route + integration in one. For finer control use create-route / create-integration separately.
Lambda integration — Proxy vs Non-proxy #
Two modes for API Gateway → Lambda.
Proxy integration (default for HTTP API) #
Pass the request mostly as-is to Lambda. Lambda returns a standard statusCode / headers / body shape.
# Event
{
"version": "2.0",
"routeKey": "POST /users",
"rawPath": "/users",
"queryStringParameters": {"limit": "10"},
"headers": {...},
"body": "{\"name\":\"Alice\"}",
"isBase64Encoded": false
}
# Lambda response
{
"statusCode": 201,
"headers": {"Content-Type": "application/json"},
"body": "{\"id\":42}"
}The production standard. The code owns its routing / response shape, so changes don’t touch API Gateway.
Non-proxy (REST API only) #
API Gateway transforms the request to Lambda’s preferred shape, transforms the response back. Defined with mapping templates (Velocity Template Language).
{
"name": "$input.path('$.name')",
"user_id": "$context.authorizer.claims.sub"
}Use when an existing Lambda wants clean inputs instead of raw events. Steep learning curve and harder to monitor — almost always use Proxy in new projects.
Routes and methods #
A route is path + method.
# Catch-all
aws apigatewayv2 create-route \
--api-id $API_ID --route-key 'ANY /'
# specific method
aws apigatewayv2 create-route \
--api-id $API_ID --route-key 'GET /users'
aws apigatewayv2 create-route \
--api-id $API_ID --route-key 'POST /users'
aws apigatewayv2 create-route \
--api-id $API_ID --route-key 'GET /users/{userId}'Parameters like {userId} come into the event’s pathParameters.
One Lambda for many routes #
For small APIs, one Lambda handles every route. Inside Lambda use a framework like FastAPI / Flask + an adapter (e.g. Mangum) for routing.
from fastapi import FastAPI
from mangum import Mangum
app = FastAPI()
@app.get("/users")
def list_users():
return [{"id": 1, "name": "Alice"}]
@app.get("/users/{user_id}")
def get_user(user_id: int):
return {"id": user_id, "name": "Alice"}
handler = Mangum(app) # Lambda handlerAPI Gateway routing is a single line: ANY /{proxy+}.
Lambda per route #
Bigger APIs split per route. Permissions / resources / deploy units become explicit.
GET /users → list-users-fn
GET /users/{id} → get-user-fn
POST /users → create-user-fnSplitting also distributes cold start — each function has its own warming and concurrency budget.
Authorization — who can call #
The most common case in API Gateway. Four options.
1) Open (no auth) #
Test / public APIs. Rare in production.
2) IAM auth #
Requires SigV4 signature on the request (AWS SDK does it automatically). Suits service-to-service inside an AWS account.
import boto3
from botocore.auth import SigV4Auth
from botocore.awsrequest import AWSRequest
import requests
session = boto3.Session()
creds = session.get_credentials()
url = "https://abc.execute-api.ap-northeast-2.amazonaws.com/hello"
req = AWSRequest(method="GET", url=url)
SigV4Auth(creds, "execute-api", "ap-northeast-2").add_auth(req)
resp = requests.get(url, headers=dict(req.headers))Calling from a browser is impractical — signing in the client would expose credentials. This option is best suited for backend-to-backend calls.
3) JWT Authorizer (HTTP API) #
API Gateway directly validates ID tokens from Cognito User Pool / Auth0 / your own OIDC. HTTP API’s strength.
aws apigatewayv2 create-authorizer \
--api-id $API_ID \
--authorizer-type JWT \
--identity-source '$request.header.Authorization' \
--jwt-configuration '{
"Audience": ["my-app"],
"Issuer": "https://cognito-idp.ap-northeast-2.amazonaws.com/<UserPoolId>"
}' \
--name jwt-authAttach to a route and Authorization: Bearer eyJ... is auto-validated; claims land in the Lambda event at requestContext.authorizer.jwt.claims.
4) Lambda Authorizer (Custom) #
You write the validation Lambda yourself. Maximum flexibility.
def handler(event, context):
token = event["headers"]["authorization"].replace("Bearer ", "")
# custom verification (DB lookup / external call)
user = verify_token_somehow(token)
if not user:
return {"isAuthorized": False}
return {
"isAuthorized": True,
"context": {"user_id": user.id, "role": user.role}
}Cacheable (skip verification for the same token within TTL). Natural for integrations with external auth systems.
5) Cognito User Pool Authorizer (REST API only) #
Direct Cognito integration in REST API. Same role as HTTP API’s JWT Authorizer but in REST API.
CORS #
Mandatory for browser callers. With HTTP API it’s one console click.
aws apigatewayv2 update-api \
--api-id $API_ID \
--cors-configuration "AllowOrigins=https://myapp.com,AllowMethods=GET,POST,AllowHeaders=*"REST API requires manual OPTIONS responses per route — the console has an “Enable CORS” button that auto-generates them.
Stages and deployments #
HTTP API #
Auto-deploy is on by default. Route changes go live immediately. There’s a single $default stage.
For dev / staging / prod separation, make a separate API per environment.
REST API #
Explicit deploys + stages. You can have dev, staging, prod stages running different versions simultaneously.
aws apigateway create-deployment --rest-api-id $REST_ID --stage-name prodEach stage can have its own caching, logging, throttling, and variables.
Throttling #
Defaults:
- Account-wide: 10,000 req/s, burst 5,000
- Per-route: configurable
aws apigatewayv2 update-stage \
--api-id $API_ID --stage-name '$default' \
--route-settings '{
"GET /heavy": {
"ThrottlingRateLimit": 100,
"ThrottlingBurstLimit": 50
}
}'Cap only the abusive / heavy routes — others stay unaffected.
API Key + Usage Plan (REST API) #
For exposing APIs to external users / partners.
API Key (per-user unique string)
│
▼
Usage Plan (limits, e.g. "100k req/month, 50 req/s")
│
▼
Stage (the actual API)Issue API Keys per user → connect to a Usage Plan → that user’s calls are auto-limited.
KEY_ID=$(aws apigateway create-api-key \
--name customer-A --enabled --query id --output text)
PLAN_ID=$(aws apigateway create-usage-plan \
--name basic-tier \
--throttle 'rateLimit=50,burstLimit=100' \
--quota 'limit=100000,period=MONTH' \
--query id --output text)
aws apigateway create-usage-plan-key \
--usage-plan-id $PLAN_ID --key-id $KEY_ID --key-type API_KEY
aws apigateway update-usage-plan \
--usage-plan-id $PLAN_ID \
--patch-operations 'op=add,path=/apiStages,value=<rest-api-id>:prod'Callers send the header x-api-key: <key>.
HTTP API doesn’t support API Key / Usage Plan. Use REST API if you need them.
Caching (REST API) #
Per-stage response caching. Natural for GET requests.
aws apigateway update-stage \
--rest-api-id $REST_ID --stage-name prod \
--patch-operations 'op=replace,path=/cacheClusterEnabled,value=true' \
--patch-operations 'op=replace,path=/cacheClusterSize,value=0.5'- Cache size: 0.5 GB – 237 GB
- TTL: per-route
- Invalidation: header
Cache-Control: max-age=0(only authorized callers)
Cost: hourly — only worth it for very high-traffic cases.
Logging — Access Log + Execution Log #
Access Log #
Per-request — for ops / business analytics.
aws apigatewayv2 update-stage \
--api-id $API_ID --stage-name '$default' \
--access-log-settings '{
"DestinationArn": "arn:aws:logs:ap-northeast-2:123456789012:log-group:/aws/apigateway/hello-api",
"Format": "{ \"requestId\":\"$context.requestId\",\"ip\":\"$context.identity.sourceIp\",\"requestTime\":\"$context.requestTime\",\"routeKey\":\"$context.routeKey\",\"status\":\"$context.status\" }"
}'JSON format — query in CloudWatch Logs Insights.
Execution Log (REST API) #
Per-stage internal logs — debugging only. Not enabled in production due to cost / noise.
Custom domain #
Expose under https://api.myapp.com/.... Wire up ACM (Intermediate #6) + Route 53 (Intermediate #5).
aws apigatewayv2 create-domain-name \
--domain-name api.myapp.com \
--domain-name-configurations \
"CertificateArn=arn:aws:acm:ap-northeast-2:...,EndpointType=REGIONAL"
# API → Domain mapping
aws apigatewayv2 create-api-mapping \
--api-id $API_ID \
--domain-name api.myapp.com \
--stage '$default'
# Route 53 — A record alias
aws route53 change-resource-record-sets ...Cost #
HTTP API #
- $1.00 / 1M requests (~$0.000001)
- Data Transfer separately
REST API #
- $3.50 / 1M requests (3.5×)
- Hourly add-on if caching is enabled
For 1M requests / month:
- HTTP API: $1
- REST API: $3.50 (+ caching / Usage Plan extras)
Not as cheap as Lambda alone, but still very affordable.
Common pitfalls #
1) Lambda doesn’t get called / permission denied #
lambda:InvokeFunction not granted to API Gateway. The console integration grants it automatically; with CLI you do aws lambda add-permission directly.
2) CORS isn’t lifting #
Browser console shows CORS error. Two places to check:
- API Gateway CORS settings (above)
Access-Control-Allow-Originheader in Lambda response (the Lambda is responsible in proxy mode)
3) Timeout #
API Gateway integration timeout: max 30 seconds (REST), 30 seconds (HTTP). Lambda allows 15 minutes but API Gateway cuts you off. For work over 30 seconds, switch to async (Lambda Destination, Step Functions, SQS).
4) Body size limit #
API Gateway payload cap is 6 MB (each direction). Use the S3 presigned URL pattern for large files.
5) Unintended auto-deploy (HTTP API) #
Route / integration changes go live immediately in production. With CI managing IaC (Terraform / CDK) it’s intentional, but direct console edits are dangerous. Console for dev / staging APIs only.
6) Audience / issuer typo on JWT Authorizer #
A single character off in aud / iss and you get 401. Confirm the precise issuer URL for Cognito User Pool ID (https://cognito-idp.<region>.amazonaws.com/<UserPoolId>).
7) API Key isn’t end-of-line security #
API Keys are transmitted in plaintext by the client. Never use them as the sole auth mechanism for sensitive operations like payments. They are for usage segmentation and throttling. Real auth is JWT or IAM.
Wrap-up #
Here is what this post covered:
- Where API Gateway fits — managed gateway in front of Lambda / ECS / external backends. Zero traffic = zero cost
- HTTP API vs REST API — HTTP is 70% cheaper + faster. REST when you need WebSocket / API Key / caching
- Lambda integration — Proxy (standard) vs Non-proxy (transform). New projects use Proxy
- Routes / methods — patterns like
GET /users/{id}. One Lambda routes all (Mangum etc.) vs split per route - Auth options — Open / IAM / JWT / Lambda Authorizer / (REST’s) Cognito User Pool
- JWT Authorizer (HTTP API) — direct validation of Cognito / Auth0 / OIDC ID tokens
- CORS — one line in HTTP API
- Stages — HTTP API auto-deploys; REST API explicit + per-environment
- Throttling — per-route caps
- API Key + Usage Plan (REST only) — per-external-user limits
- Caching (REST only) — per-stage
- Access Log — JSON to CloudWatch Logs
- Custom Domain — ACM + Route 53
- Cost — HTTP API at $0.000001 per request, very cheap
- Pitfalls — Lambda permission, CORS, 30s timeout, 6MB payload, HTTP API auto-deploy, JWT issuer typo, role of API Key
Up next — messaging infrastructure #
The synchronous side of API Gateway / Lambda is done. Now it’s time for asynchronous / event-driven communication.
#5 EventBridge / SQS / SNS covers the three side by side, fan-out patterns, DLQ, and idempotency — AWS’s messaging infrastructure, all in one piece.