Serving Lambda functions to the outside world through API Gateway: REST and HTTP APIs, resources and methods, Lambda proxy integration, stages, custom domains, and API key and JWT authorizers.

In episode 8 we made messages flow between services. But your application needs more than that: it needs an entry point that can be called from a browser, a mobile app, or external clients. On AWS, that door is called API Gateway — a single gateway that receives HTTP requests, handles authentication and throttling, then forwards them to Lambda.
Think of API Gateway as an office receptionist: all guests enter through one door, get their identity verified, and are then directed to the right room (backend). In this episode you'll build that receptionist on LocalStack.
AWS has two generations of API Gateway: REST API (comprehensive, feature-rich) and HTTP API (lightweight, cheaper, faster). Choose based on your needs:
| Aspect | REST API | HTTP API |
|---|---|---|
| Features | Comprehensive, mapping templates, usage plans | Simple, fast to launch |
| Authorizer | Custom Lambda, Cognito | Built-in JWT, Lambda |
| Custom domain | Yes | Yes |
| Price | More expensive | About 70% cheaper |
| When to use | Complex backends, enterprise needs | Modern APIs, cost-sensitive |
LocalStack supports both. The REST API is accessed via awslocal apigateway, the HTTP API via awslocal apigatewayv2.
Creating a REST API takes several steps: create the API, create the resource (path), attach a method, then integrate it with Lambda. Prepare the Lambda function first:
zip -r function.zip handler.py
awslocal lambda create-function --function-name orders \
--runtime python3.12 --role arn:aws:iam::000000000000:role/lambda-role \
--handler handler.handler --zip-file fileb://function.zipNow assemble the API. Store the API ID in a variable so you don't retype it:
API_ID=$(awslocal apigateway create-rest-api --name orders-api | jq -r '.id')
ROOT_ID=$(awslocal apigateway get-resources --rest-api-id $API_ID | jq -r '.items[0].id')
RES_ID=$(awslocal apigateway create-resource --rest-api-id $API_ID \
--parent-id $ROOT_ID --path-part orders | jq -r '.id')
awslocal apigateway put-method --rest-api-id $API_ID \
--resource-id $RES_ID --http-method GET --authorization-type NONE
awslocal apigateway put-integration --rest-api-id $API_ID \
--resource-id $RES_ID --http-method GET --type AWS_PROXY \
--integration-http-method POST \
--uri arn:aws:apigateway:us-east-1:lambda:path/2015-03-31/functions/arn:aws:lambda:us-east-1:000000000000:function:orders/invocationsThe AWS_PROXY type means the entire request is forwarded raw to Lambda, and the Lambda output is returned to the client. This is the most popular mode because you control the response entirely in your code.
API changes don't take effect until they're deployed to a stage. A stage is an "environment" (dev, staging, prod) of the same API:
awslocal apigateway create-deployment --rest-api-id $API_ID --stage-name devA REST API in LocalStack can be called via the URL http://localhost:4566/restapis/<API_ID>/<stage>/<path>:
curl http://localhost:4566/restapis/$API_ID/dev/ordersTip
Don't forget that every change to a method or integration requires awslocal apigateway create-deployment again so the stage is updated — the classic bug where an endpoint seems "unchanged" even though the Lambda code was replaced.
In production, APIs are called through your own domain, not a long restapis/... URL. LocalStack simulates custom domains via the Host header:
awslocal apigateway create-domain-name --domain-name api.localhost
awslocal apigateway create-base-path-mapping \
--domain-name api.localhost --rest-api-id $API_ID --stage dev
curl -H 'Host: api.localhost' http://localhost:4566/dev/ordersWith this pattern, client code just swaps the base URL to http://api.localhost:4566 without knowing the API Gateway details.
To restrict access, the REST API supports API keys via usage plans. Create a key, a plan, then link them:
KEY_ID=$(awslocal apigateway create-api-key --name dev-key --enabled | jq -r '.id')
PLAN_ID=$(awslocal apigateway create-usage-plan --name dev-plan | jq -r '.id')
awslocal apigateway create-usage-plan-key \
--usage-plan-id $PLAN_ID --key-id $KEY_ID --key-type API_KEYThe HTTP API offers a built-in JWT authorizer. The client sends a token in the Authorization header, and the API validates the signature and audience without any extra code:
HTTP_ID=$(awslocal apigatewayv2 create-api --protocol-type HTTP \
--name orders-http | jq -r '.ApiId')
awslocal apigatewayv2 create-authorizer --api-id $HTTP_ID \
--authorizer-type JWT --name jwt-auth \
--identity-source '$request.header.Authorization' \
--jwt-configuration '{"Audience":["api"],"Issuer":"https://issuer.example.com"}'
awslocal apigatewayv2 create-stage --api-id $HTTP_ID --stage-name devWarning
The string $request.header.Authorization must be quoted with single quotes in the shell. Without quotes, the shell will expand $request into an empty string and the authorizer breaks silently.
The AWS_PROXY integration forwards everything raw. If you need transformation — for example hiding internal fields or changing status codes — the REST API provides mapping templates. Here's an example mapping that wraps the Lambda response in a particular shape:
{
"application/json": "#set($response = $input.path('$.body')){\"envelope\":$response}"
}This mapping is attached via put-integration-response on the resource. Its job: a thin middleware before the response reaches the client, without touching the Lambda code at all.
Summary of this episode:
awslocal apigateway) for full features; HTTP API (awslocal apigatewayv2) for lightweight JWT-based APIs.create-rest-api, create-resource, put-method, put-integration of type AWS_PROXY, then create-deployment.http://localhost:4566/restapis/<id>/<stage>/<path>.Host header, e.g. api.localhost.Your API now has a public face. But building APIs command by command won't scale — teams need infrastructure that can be documented and reproduced. In episode 10 we automate everything with Infrastructure as Code: CloudFormation, Terraform, and SAM. See you there!