Build and Deploy a Serverless REST API with AWS Lambda, API Gateway, and SAM CLI
Scaffold, run locally, and ship a typed Python REST API to AWS in under 30 minutes using SAM templates and a single deploy command.
What You'll Build
A typed Python REST API with two GET endpoints, running on AWS Lambda behind an API Gateway stage named v1, defined in a SAM template and testable locally before a one-command deploy.
Prerequisites
- AWS account with IAM permissions covering CloudFormation, Lambda, API Gateway, IAM role creation, and S3
- AWS CLI v2 configured with credentials (
aws configureor an SSO profile) - SAM CLI 1.100 or later
- Docker Desktop (required by
sam local) - Python 3.12 available locally is helpful but not strictly required; Lambda runs its own runtime
Install SAM CLI:
macOS (Homebrew):
brew install aws/tap/aws-sam-cli
Linux x86_64 (official installer):
curl -Lo aws-sam-cli.zip https://github.com/aws/aws-sam-cli/releases/latest/download/aws-sam-cli-linux-x86_64.zip
unzip aws-sam-cli.zip -d sam-installation
sudo ./sam-installation/install
Verify:
sam --version
# SAM CLI, version 1.x.x
1. Scaffold the Project
Skip sam init and its boilerplate. Create the structure manually:
mkdir items-api && cd items-api
mkdir src
touch template.yaml src/app.py src/requirements.txt
items-api/
├── template.yaml
└── src/
├── app.py
└── requirements.txt
2. Write the SAM Template
The ItemsApi resource pins the stage name to v1, which becomes part of the invoke URL. Referencing it explicitly with RestApiId: !Ref ItemsApi in each event ties the function to your named API rather than the implicit one SAM creates when you omit that field. That distinction matters once you need to attach auth, CORS, or request validators.
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: Items REST API
Globals:
Function:
Runtime: python3.12
Timeout: 10
MemorySize: 128
Resources:
ItemsApi:
Type: AWS::Serverless::Api
Properties:
StageName: v1
Description: Items API v1
ItemsFunction:
Type: AWS::Serverless::Function
Properties:
Handler: app.handler
CodeUri: src/
Architectures:
- x86_64
Description: Get items collection and single item by ID
Events:
GetItems:
Type: Api
Properties:
RestApiId: !Ref ItemsApi
Path: /items
Method: GET
GetItem:
Type: Api
Properties:
RestApiId: !Ref ItemsApi
Path: /items/{id}
Method: GET
Outputs:
ApiUrl:
Description: Base API URL
Value: !Sub "https://${ItemsApi}.execute-api.${AWS::Region}.amazonaws.com/v1"
3. Write the Python Handler
API Gateway's Lambda proxy integration requires the handler to return a dict with statusCode (integer), headers, and body (a JSON-encoded string). Using TypedDict makes that contract explicit and catches shape mistakes at type-check time rather than at runtime.
import json
from typing import Any, TypedDict
class LambdaResponse(TypedDict):
statusCode: int
headers: dict[str, str]
body: str
_JSON_HEADERS = {"Content-Type": "application/json"}
_ITEMS: dict[str, dict[str, str]] = {
"1": {"id": "1", "name": "Widget"},
"2": {"id": "2", "name": "Gadget"},
}
def handler(event: dict[str, Any], context: Any) -> LambdaResponse:
path_params: dict[str, str] = event.get("pathParameters") or {}
item_id = path_params.get("id")
if item_id:
item = _ITEMS.get(item_id)
if not item:
return {
"statusCode": 404,
"headers": _JSON_HEADERS,
"body": json.dumps({"error": "not found"}),
}
return {
"statusCode": 200,
"headers": _JSON_HEADERS,
"body": json.dumps(item),
}
return {
"statusCode": 200,
"headers": _JSON_HEADERS,
"body": json.dumps({"items": list(_ITEMS.values())}),
}
Leave src/requirements.txt empty. No third-party dependencies here.
4. Build and Run Locally
sam build installs dependencies from requirements.txt and stages your code into .aws-sam/build/. Run it before any sam local command.
sam build
For a quick smoke test, generate a minimal API Gateway proxy event, save it to a file, and invoke the function:
sam local generate-event apigateway aws-proxy --method GET > event.json
sam local invoke ItemsFunction --event event.json
The first run pulls the public.ecr.aws/lambda/python:3.12 Docker image, which takes a minute. You should see output ending with:
{"statusCode": 200, "headers": {"Content-Type": "application/json"}, "body": "{\"items\": [{\"id\": \"1\", \"name\": \"Widget\"}, {\"id\": \"2\", \"name\": \"Gadget\"}]}"}
To run the full API Gateway emulation:
sam local start-api --port 3000
In a second terminal:
curl http://localhost:3000/items
curl http://localhost:3000/items/1
curl http://localhost:3000/items/99 # expect 404
Note that sam local start-api serves without the stage prefix, so the path is /items, not /v1/items. Code changes require a fresh sam build before the next request picks them up.
5. Deploy to AWS
sam deploy --guided
Fill in the prompts:
| Prompt | Recommended answer |
|---|---|
| Stack Name | items-api |
| AWS Region | your target region |
| Confirm changes before deploy | Y |
| Allow SAM CLI IAM role creation | Y |
| Disable rollback | N |
GetItems may not have authorization defined, Is this okay? |
Y |
GetItem may not have authorization defined, Is this okay? |
Y |
| Save arguments to samconfig.toml | Y |
The two authorization prompts appear because neither endpoint has an Auth property set. The default answer is N, which aborts the deployment, so you must explicitly answer Y for both. These are open read endpoints and that's intentional here. If you later add auth (a Cognito authorizer, for example), these prompts disappear.
SAM packages build artifacts into a managed S3 bucket, creates a CloudFormation changeset, and streams stack events to the terminal. The final output includes your Outputs block. After this first run, samconfig.toml holds the answers and future deploys need only sam deploy.
Verify It Works
Copy the ApiUrl from the Outputs block and run:
BASE="https://<api-id>.execute-api.<region>.amazonaws.com/v1"
curl -i "${BASE}/items"
curl -i "${BASE}/items/2"
curl -i "${BASE}/items/99"
Expect 200 with the items array, 200 with a single item object, and 404 with {"error": "not found"}. The -i flag lets you confirm Content-Type: application/json is present in response headers.
To tail Lambda logs directly from the SAM CLI:
sam logs --stack-name items-api --tail
Troubleshooting
sam local fails with "Running AWS SAM projects locally requires Docker."
Docker Desktop must be running before any sam local command. On Linux, add your user to the docker group (sudo usermod -aG docker $USER) and open a fresh shell session.
sam deploy exits with a CAPABILITY_IAM error.
SAM creates an IAM execution role for the Lambda function. Your credentials need iam:CreateRole and iam:AttachRolePolicy. During --guided, answer Y to "Allow SAM CLI IAM role creation" to pass the capability flag automatically.
Lambda returns 502 Bad Gateway after a successful deploy.
Almost always means body is a dict instead of a string. API Gateway's proxy integration requires body to be a JSON-encoded string. Verify every return path calls json.dumps(). Check CloudWatch Logs for the raw exception.
ModuleNotFoundError in Lambda but works locally.
Confirm CodeUri in the template points to the directory that contains requirements.txt. If you added C-extension packages later and you're developing on macOS Apple Silicon, rebuild with sam build --use-container to compile against the Lambda x86_64 environment.
Next Steps
- Add a DynamoDB table with
AWS::Serverless::SimpleTableand replace the in-memory dict withboto3resource calls. - Configure request validation on the
AWS::Serverless::Apiresource usingModelsandRequestModelsto reject malformed input before Lambda runs. - Run
sam pipelineto generate a GitHub Actions or CodePipeline config for automated deploys on merge. - Pull in
aws-lambda-powertoolsfor structured JSON logging, X-Ray tracing, and strongly typed event models includingAPIGatewayProxyEventwith full attribute hints.
Emeka has spent over a decade tracking threat actors, vulnerability disclosures, and the evolving landscape of application security, bringing a sharp continent-spanning perspective to his reporting. He's known for translating dense CVE advisories into clear, actionable context that developers and security teams alike actually read.
Discussion 0
No comments yet
Be the first to weigh in.