API Gateway + Lambda
The standard pattern for exposing Lambda over HTTP. We cover the difference between REST API and HTTP API, Lambda integration (proxy / non-proxy), routes / methods, authorization (IAM / Cognito / Lambda authorizer), CORS, stages / deployment, throttling, usage plans, caching, custom domains, and cost.
To call the function from Chapter 17 Lambda basics over HTTP, you need an entrance. The AWS standard is API Gateway — a managed gateway you put in front of Lambda (or ECS, EC2, an external backend).
This chapter puts together, all at once, API Gateway’s two forms (REST / HTTP), Lambda integration patterns, authorization options, stages / deployment, and usage plans. The synchronous invocation model we set up in the previous Chapter 17 becomes an actual HTTP endpoint here, and the next Chapter 19 EventBridge / SQS / SNS crosses over to the opposite side — asynchronous communication.
What API Gateway does #
client (browser, app)
│ HTTPS
▼
API Gateway
├─ TLS termination
├─ routing (path → backend)
├─ authentication / authorization
├─ rate limiting / throttling
├─ request transformation
└─ caching
│
▼
backend (Lambda / ECS / external HTTP)It’s similar in function to ALB (Chapter 13 ALB / NLB and ACM) but with a different emphasis.
| ALB | API Gateway | |
|---|---|---|
| Pricing model | Per hour + LCU | Per request + data |
| Cost at 0 traffic | Per hour (~$20/month) | 0 |
| Direct Lambda integration | Possible | Natural |
| API key / Usage Plan | None | Yes |
| Caching | None | Yes (REST API) |
| Cognito integration | Separate | Natural |
| Fits | Always-on large ECS | Variable / serverless / needs API key |
REST API vs HTTP API #
API Gateway comes in two forms.
HTTP API (v2) — the newer form #
- 70% cheaper and faster.
- Validates JWT tokens (Cognito / Auth0 / your own OIDC) directly.
- CORS setup is simple.
- Lambda proxy integration is the default.
- No WebSocket, no API key / Usage Plan, no request transformation / caching.
The default candidate for a new project. Enough for a simple REST API + JWT.
REST API (v1) — the older form + rich features #
- Caching (per stage)
- Request / response mapping transformation
- Direct WAF integration
- API Key + Usage Plan
- WebSocket support
Use it when you need complex transformation / usage separation / WebSocket.
How to choose #
Need WebSocket? ─Yes→ REST API (or WebSocket API)
│ No
▼
Need API Key / Usage Plan? ─Yes→ REST API
│ No
▼
Need complex transformation / caching? ─Yes→ REST API
│ No
▼
HTTP API (cheap + fast)This chapter proceeds on an HTTP API basis and flags REST-API-only features separately.
First API — Hello, World #
Prepare the 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 the function we already covered in Chapter 17 Lambda basics. Deploy this function as hello-fn.
Create the HTTP API (console) #
Console → API Gateway → “Create API” → “Build” on HTTP API.
- Add integration: Lambda → region / function (
hello-fn) - API name:
hello-api - Configure routes:
GET /hello→hello-fn - Stage:
$default(auto deploy) - Create
Once done, you get a URL: https://abc123.execute-api.ap-northeast-2.amazonaws.com/hello?name=AWS
Create it with the CLI #
# create the API
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 Lambda to be invoked by API Gateway
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 option is a shortcut that does the route and integration automatically. If you need fine control, use create-route and create-integration separately.
Lambda integration — Proxy vs Non-proxy #
The two modes of an API Gateway → Lambda invocation.
Proxy integration (the HTTP API default) #
Passes the request to Lambda almost as-is. Lambda returns statusCode / headers / body in the standard response 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 operational standard. If the code takes responsibility for its own routing / response shaping, you don’t have to touch API Gateway when you change things.
Non-proxy (REST API only) #
API Gateway transforms the request into the shape Lambda wants and transforms the response too. You define it with a mapping template (Velocity Template Language).
{
"name": "$input.path('$.name')",
"user_id": "$context.authorizer.claims.sub"
}It lets an existing Lambda receive clean input instead of the raw event. There’s a learning curve and monitoring is tricky, so new projects almost always use Proxy.
Routes and methods #
A route = a path + method combination.
# Catch-all
aws apigatewayv2 create-route \
--api-id $API_ID --route-key 'ANY /'
# specific methods
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}'A parameter like {userId} comes into the event’s pathParameters.
One Lambda for several routes #
For a small API, one Lambda handles all routes. Inside the Lambda you route with a framework like FastAPI / Flask and an adapter (e.g., Mangum).
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 handlerThe API Gateway route is a single line: ANY /{proxy+}.
A different Lambda per route #
Large APIs are split by route, and the unit of permission, resource, and deployment becomes clear.
GET /users → list-users-fn
GET /users/{id} → get-user-fn
POST /users → create-user-fnSplitting also distributes the cold starts — you manage warming / concurrency separately per function.
Authorization — who can call it #
The setting you hit most often in API Gateway. There are four options.
1) Open (no authentication) #
For tests / public APIs. Rarely used in operations.
2) IAM authentication #
The request needs a SigV4 signature (the AWS SDK does it automatically). Suitable for service-to-service calls within 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 directly from a browser is hard (signature exposure). It’s for backend ↔ backend use.
3) JWT Authorizer (HTTP API) #
API Gateway validates the ID token of a Cognito User Pool / Auth0 / your own OIDC directly. This is 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 it to a route, and the Authorization: Bearer eyJ... header is validated automatically, with claims coming into the Lambda event’s requestContext.authorizer.jwt.claims.
4) Lambda Authorizer (Custom) #
A Lambda you write takes responsibility for the validation logic itself. Offers maximum flexibility.
def handler(event, context):
token = event["headers"]["authorization"].replace("Bearer ", "")
# your own validation logic (DB lookup / external call, etc.)
user = verify_token_somehow(token)
if not user:
return {"isAuthorized": False}
return {
"isAuthorized": True,
"context": {"user_id": user.id, "role": user.role}
}Caching is possible (it skips validation for the same token during the TTL). Natural for integrating with an external authentication system.
5) Cognito User Pool Authorizer (REST API only) #
Integrates Cognito directly in REST API. Similar in function to HTTP API’s JWT Authorizer, but used in REST API.
CORS #
To call from a browser, CORS setup is required. HTTP API is done in one console click.
aws apigatewayv2 update-api \
--api-id $API_ID \
--cors-configuration "AllowOrigins=https://myapp.com,AllowMethods=GET,POST,AllowHeaders=*"For REST API, you have to build the OPTIONS response for each route yourself — the console generates an “Enable CORS” button automatically.
Stages and deployment #
HTTP API #
Auto-deploy is the default option. Route changes are reflected immediately. There’s one stage, $default.
To separate other environments (dev / staging / prod), the direction is to create separate APIs.
REST API #
Uses explicit deployment and stages. You can run different versions simultaneously on stages like dev, staging, prod.
aws apigateway create-deployment --rest-api-id $REST_ID --stage-name prodPer stage, you set caching, logging, throttling, and variables differently.
Throttling — limiting traffic #
The default limits are as follows.
- Account level: 10,000 req/s, burst 5,000
- Route level: separately configurable
aws apigatewayv2 update-stage \
--api-id $API_ID --stage-name '$default' \
--route-settings '{
"GET /heavy": {
"ThrottlingRateLimit": 100,
"ThrottlingBurstLimit": 50
}
}'Limit only the malicious / heavy routes strongly — other routes are unaffected.
API Key + Usage Plan (REST API) #
For exposing an API to external users / partners.
API Key (a per-user unique string)
│
▼
Usage Plan (limits, e.g., "100k req/month, 50 req/sec")
│
▼
Stage (the actual API)On user registration, issue an API Key → connect it to a Usage Plan → that user’s requests automatically get the limit applied.
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'The calling side sends the header x-api-key: <key> along with the request.
HTTP API doesn’t support API Key / Usage Plan. Use REST API if you need it.
Caching (REST API) #
Caches responses per stage. 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: configured per route
- Invalidation: header
Cache-Control: max-age=0(only callers with the needed permission)
The cost is per hour — it’s worth it only when traffic is truly heavy.
Logging — Access Log + Execution Log #
Access Log #
Per request — used for operational / business analysis.
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\" }"
}'Receive it as JSON and query it in CloudWatch Logs Insights.
Execution Log (REST API) #
Internal per-step logs — for debugging. In operations, it’s rarely turned on due to cost / noise.
Custom Domain #
Expose it as https://api.myapp.com/.... Connect with an ACM certificate (Chapter 13 ALB / NLB and ACM) and Chapter 12 Route 53.
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 separate
REST API #
- $3.50 / 1M requests (3.5x)
- Plus per hour when caching is enabled
For 1M requests/month.
- HTTP API: $1
- REST API: $3.50 (+ extra when using caching / Usage Plan)
Not as cheap as Lambda, but very cheap.
Pitfalls you’ll often hit #
1) Lambda isn’t invoked / permission denied #
The lambda:InvokeFunction permission wasn’t granted to API Gateway. Console integration is automatic; with the CLI you do aws lambda add-permission yourself.
2) CORS won’t resolve #
A CORS error appears in the browser console. Check two things.
- The API Gateway CORS setting (see above)
- The
Access-Control-Allow-Originheader in the Lambda response (with proxy integration, Lambda is responsible)
3) Timeout #
API Gateway’s integration timeout is up to 30 seconds (REST), 30 seconds (HTTP). Lambda is 15 minutes, but API Gateway cuts it off. Work around processing over 30 seconds asynchronously (Lambda Destination, Step Functions, SQS).
4) Body size limit #
API Gateway’s payload limit is 6 MB (both sides). Use the S3 presigned URL pattern for large files.
5) Unintended auto deploy (HTTP API) #
Route / integration changes are reflected in production immediately. Managed with IaC (Terraform / CDK) in CI it’s an intended change, but direct console edits are dangerous. Use the console only for dev / staging APIs.
6) A typo in the JWT Authorizer’s audience / issuer #
If aud / iss differs by even one character, it’s a 401. Check the exact issuer URL of the Cognito User Pool ID (https://cognito-idp.<region>.amazonaws.com/<UserPoolId>).
7) An API Key isn’t the end of all security #
An API Key remains in plaintext on the client — don’t use it as the sole authentication for things like payment information. It’s for usage separation and throttling. Real authentication is JWT / IAM.
Exercises #
- For the API you’ll build, follow the decision tree in §“REST API vs HTTP API” to decide between HTTP API and REST API, and write in one sentence the single feature (WebSocket / API Key / caching / cost) that decided it.
- Compare in one paragraph the pros and cons of handling a small API in one Lambda with FastAPI + Mangum versus splitting a Lambda per route, basing it on §“Routes and methods” and the cold start of Chapter 17 Lambda basics.
- Among the four options in §“Authorization,” pick the one suitable for an API called directly from a browser SPA and the one suitable for an API called between backends, and write why for each. If you use a JWT Authorizer, also write what the most common cause of a 401 is from §“Pitfalls you’ll often hit.”
In short: API Gateway is a managed gateway you put in front of Lambda / ECS / an external backend, and its cost at 0 traffic is 0. HTTP API is 70% cheaper and faster than REST API and is the default for new projects, and you use REST API when you need WebSocket, API Key, or caching. Proxy is the standard for Lambda integration, authorization splits into Open / IAM / JWT / Lambda Authorizer, and HTTP API’s JWT Authorizer validates Cognito, Auth0, and OIDC tokens directly. The most common pitfalls are a missing Lambda invoke permission, CORS, the 30-second timeout, the 6 MB payload limit, and a JWT issuer typo.
Next chapter #
API Gateway and Lambda’s synchronous invocation is done. The next Chapter 19 EventBridge / SQS / SNS covers asynchronous / event-driven communication. It puts together AWS’s messaging infrastructure: a role comparison of the three tools, the fan-out pattern, DLQs, and idempotency.