API Gateway puts an HTTP endpoint in front of a Lambda, and handles TLS, custom domains, throttling and authentication on the way. This site's admin API is a FastAPI application behind one.
HTTP API or REST API
Confusingly, both are REST APIs in the ordinary sense. These are two products.
| HTTP API | REST API | |
|---|---|---|
| Price | About 30% of REST | Higher |
| Latency | Lower | Higher |
| JWT authorizer | Built in | Lambda authorizer needed |
| Request/response transformation | No | Yes, VTL templates |
| Request validation | No | Yes |
| API keys and usage plans | No | Yes |
| Private endpoints, WAF | Limited | Yes |
Use an HTTP API unless you need something only REST has. For a backend where the Lambda does its own routing and validation — any web framework — the extra features are things you would not use, at three times the price.
Pick REST when you need API keys with usage plans for third-party consumers, request validation at the edge, or VTL transformation to talk to a legacy client.
Proxy integration and the event shape
The useful default is proxy integration: API Gateway forwards the whole request as JSON and returns whatever your function produces, with no mapping in between.
{
"version": "2.0",
"routeKey": "GET /posts/{slug}",
"rawPath": "/posts/aws-lambda",
"headers": { "authorization": "Bearer ..." },
"queryStringParameters": { "draft": "true" },
"pathParameters": { "slug": "aws-lambda" },
"body": null,
"isBase64Encoded": false,
"requestContext": { "http": { "method": "GET", "sourceIp": "203.0.113.7" } }
}That is payload format 2.0, used by HTTP APIs. REST APIs use 1.0, which puts
the method at httpMethod and headers in a different shape. Handler code written for
one silently reads None from the other — a common source of "it worked in the tutorial"
confusion. Adapters like Mangum and Serverless Express handle both, which is why running a normal
web framework on Lambda works at all.
Your response must be the right shape too — statusCode, optional
headers, and a string body. Returning a dict as
body produces a 502, because API Gateway cannot interpret it.
The stage trap that 404s one of your two URLs
This one costs an afternoon and the error message never mentions it.
Every API is deployed to a stage, and the default execute-api URL includes the stage name in the path:
https://abc123.execute-api.us-west-2.amazonaws.com/prod/posts
^^^^^ the stageA custom domain does not. It maps to a stage, so the stage name is not in the path:
https://api.lovemesomecoding.com/postsSo a route registered as /prod/posts works on one URL and 404s on the other, and
whichever you tested is the one that works.
The fix is the $default stage, which has no name and therefore
adds no prefix. Both URLs then serve identical paths. This site's API uses it, and the deploy
script asserts it.
aws apigatewayv2 get-stages --api-id abc123 \
--query "Items[].[StageName,AutoDeploy]" --output tableRoutes
A route is a method and a path. For a framework doing its own routing, one catch-all is all you need:
aws apigatewayv2 create-route --api-id abc123 \
--route-key '$default' \
--target integrations/xyz789Everything reaches the function and FastAPI, Express or Flask decides what to do. The alternative — a route per endpoint — gives you per-route authorizers and throttling, at the cost of keeping the gateway and your application in sync. Pick one; splitting routing across both is how you end up with a 404 nobody can locate.
CORS: configure it in one place
Both API Gateway and your application can answer preflight requests, and if both do they will eventually disagree — the gateway allows an origin the app rejects, or the preflight succeeds and the real request fails.
Pick one owner. This site lets FastAPI handle CORS entirely, so preflight and real requests cannot diverge, and the gateway does nothing. If you let the gateway own it, turn the framework's CORS middleware off.
Authorizers
Three ways to decide whether a request is allowed, before your function runs.
JWT authorizer (HTTP APIs) — you give it an issuer and audience, it validates the token's signature, expiry and claims, and rejects anything invalid at the gateway. Your function never sees a bad token and never needs the validation code. This is the reason most teams pick an HTTP API.
Lambda authorizer — your own function returns an allow or deny. Necessary for anything unusual, and note the result is cached by identity source for a configurable TTL, so a revoked token keeps working until the cache expires.
IAM authorization — callers sign requests with SigV4. Right for service-to-service inside AWS, wrong for browsers.
Throttling and timeouts
The hard limit worth knowing: 29 seconds. Integration timeout is capped there, so a Lambda configured for 15 minutes behind an API Gateway still gets 29 seconds. Anything longer needs a different shape — return 202, do the work asynchronously, and let the client poll.
Throttling is on by default at the account level and configurable per stage or route. Setting a sane rate limit is worth doing early: it is what stops one misbehaving client from exhausting your Lambda concurrency and taking the whole API down with it.
When you get a 502 or a 403
Two status codes account for most confusion, and neither means what it appears to.
A 502 Bad Gateway is almost never a network problem. It means your function
returned something API Gateway could not turn into an HTTP response — a dict where a string was
expected, a missing statusCode, or an unhandled exception. The Lambda log has the real
error; the gateway only knows the shape was wrong.
A 403 Forbidden with Missing Authentication Token is the most
misleading message in the service. It usually has nothing to do with authentication: it is what you
get when no route matches the path or method you requested. Check the route key before you check
your token.