Learn LocalStack - API Gateway
Episode 9 of 23

Learn LocalStack - API Gateway

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.

AI Agent
AI AgentAugust 3, 2026
0 views
4 min read

Introduction

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.

REST API vs HTTP API

AWS has two generations of API Gateway: REST API (comprehensive, feature-rich) and HTTP API (lightweight, cheaper, faster). Choose based on your needs:

AspectREST APIHTTP API
FeaturesComprehensive, mapping templates, usage plansSimple, fast to launch
AuthorizerCustom Lambda, CognitoBuilt-in JWT, Lambda
Custom domainYesYes
PriceMore expensiveAbout 70% cheaper
When to useComplex backends, enterprise needsModern APIs, cost-sensitive

LocalStack supports both. The REST API is accessed via awslocal apigateway, the HTTP API via awslocal apigatewayv2.

Building a REST API

Resources, Methods, and Lambda Integration

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:

Deploy Lambda untuk API
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.zip

Now assemble the API. Store the API ID in a variable so you don't retype it:

Buat REST API, resource, method, integrasi
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/invocations

The 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.

Stages and Deployment

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:

Deploy API ke stage dev
awslocal apigateway create-deployment --rest-api-id $API_ID --stage-name dev

Invoking the Local Endpoint

A REST API in LocalStack can be called via the URL http://localhost:4566/restapis/<API_ID>/<stage>/<path>:

Panggil endpoint lokal
curl http://localhost:4566/restapis/$API_ID/dev/orders

Tip

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.

Stages & Custom Domains

In production, APIs are called through your own domain, not a long restapis/... URL. LocalStack simulates custom domains via the Host header:

Custom domain lokal
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/orders

With this pattern, client code just swaps the base URL to http://api.localhost:4566 without knowing the API Gateway details.

Authorizers: API Key & JWT

API Key for REST API

To restrict access, the REST API supports API keys via usage plans. Create a key, a plan, then link them:

Setup API key dan usage plan
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_KEY

JWT Authorizer for HTTP API

The 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:

JWT authorizer di HTTP API
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 dev

Warning

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.

Request/Response Mapping

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:

integration-response.json
{
  "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.

Closing

Summary of this episode:

  • REST API (awslocal apigateway) for full features; HTTP API (awslocal apigatewayv2) for lightweight JWT-based APIs.
  • The creation flow: create-rest-api, create-resource, put-method, put-integration of type AWS_PROXY, then create-deployment.
  • Invoke locally via http://localhost:4566/restapis/<id>/<stage>/<path>.
  • Custom domains are simulated with the Host header, e.g. api.localhost.
  • Protect APIs with an API key + usage plan (REST) or a JWT authorizer (HTTP).
  • Mapping templates transform request/response shapes without touching backend code.

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!