DEV Community

Cover image for πŸ—οΈ Part 2 β€” Running a Serverless API Locally with AWS SAM (API Gateway + Lambda + DynamoDB)
Balaji Sivakumar
Balaji Sivakumar

Posted on

πŸ—οΈ Part 2 β€” Running a Serverless API Locally with AWS SAM (API Gateway + Lambda + DynamoDB)

πŸ“˜ Serverless TODO App β€” Article Series

Part Title
1 Architecture Overview
2 Local Backend with AWS SAM (You are here)
3 Deploying Backend to AWS (SAM + CDK) - (Coming soon…)

πŸ—οΈ Part 2 β€” Running a Serverless API Locally with AWS SAM

(API Gateway + Lambda + DynamoDB Local)

This article expands on Part 1, diving into how the backend works and how to run it fully locally using:

  • AWS SAM
  • Local Lambda (Docker)
  • DynamoDB Local
  • Seed scripts
  • curl-based testing

This ensures rapid, cost-free development before deploying to AWS (in Part 3).

πŸ”— GitHub Repo:

πŸ‘‰ aws-sam-gateway-lambda-dynamodb

Scope: Part 2 is local-only. Cloud deploy, auth, and hardening land in Part 3.


🧰 Prerequisites for Local-Only


🎯 What This Part Covers

  • How Lambda routing, validation, and DB access work together
  • How SAM emulates API Gateway + Lambda
  • How DynamoDB Local integrates via environment variables
  • Full local run workflow
  • curl commands to test every endpoint

🧭 Why Local-First Development?

Local-first gives:

  • Instant iteration
  • No AWS costs
  • Offline development
  • Safe experimentation
  • Faster debugging
  • Confidence before cloud deployment

SAM provides a near-identical Lambda runtime locally using Docker.


πŸ—οΈ Architecture Overview & What You’ll Build

Single Lambda handles all /todos routes, talking to DynamoDB Local via SAM’s local API Gateway:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ API Gateway (Local)      β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
               β”‚
        /todos routes
               β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ AWS Lambda (Python 3.13) β”‚  <-- src/app.py
β”‚ Router + Handlers (Local)β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
               β”‚ boto3
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ DynamoDB (Local)         β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
Enter fullscreen mode Exit fullscreen mode

🧩 What the Backend Does

CRUD API for TODO items:

Method Route Description
GET /todos List all items
GET /todos/{id} Get by ID
POST /todos Create
PUT /todos/{id} Update
DELETE /todos/{id} Delete

🧠 Deep Dive β€” How the Lambda Works

Below are the essential excerpts needed to understand the backend logic.


1️⃣ Lambda Router (src/app.py)

def handler(event, context):
    path = event.get("resource") or event.get("path", "")
    http_method = event.get("httpMethod", "GET").upper()
    path_params = event.get("pathParameters") or {}

    if path == "/todos" and http_method == "POST":
        return create_todo(event)
    if path == "/todos" and http_method == "GET":
        return list_todos(event)
    if path == "/todos/{id}" and http_method == "GET":
        return get_todo(path_params.get("id"))
    if path == "/todos/{id}" and http_method == "PUT":
        return update_todo(path_params.get("id"), event)
    if path == "/todos/{id}" and http_method == "DELETE":
        return delete_todo(path_params.get("id"))

    return _response(404, {"message": "Not Found"})
Enter fullscreen mode Exit fullscreen mode

βœ” Simple but effective routing

βœ” One Lambda for all endpoints

βœ” Fast & cost-efficient


2️⃣ Pydantic Models (src/models.py)

class TodoCreate(BaseModel):
    title: str = Field(min_length=1)
    description: Optional[str] = None
    status: str = Field(default="pending")  # pending|done

class TodoUpdate(BaseModel):
    title: Optional[str] = None
    description: Optional[str] = None
    status: Optional[str] = None
Enter fullscreen mode Exit fullscreen mode

βœ” Ensures clean validation

βœ” Protects API from malformed payloads


3️⃣ DynamoDB Layer (src/ddb.py)

ddb = boto3.resource(
    "dynamodb",
    endpoint_url=os.getenv("DDB_ENDPOINT")  # Local or AWS
)

table = ddb.Table(os.getenv("TABLE_NAME"))
Enter fullscreen mode Exit fullscreen mode

This makes the DB layer environment-agnostic.


4️⃣ Example Handler β€” Create Todo

def create_todo(data):
    payload = TodoCreate(**json.loads(data.get("body") or "{}"))

    item = {
        "id": str(uuid.uuid4()),
        "title": payload.title,
        "description": payload.description,
        "status": payload.status,
        "created_at": TodoItem.now_iso(),
        "updated_at": TodoItem.now_iso(),
    }

    table.put_item(Item=item)

    return _response(201, item)
Enter fullscreen mode Exit fullscreen mode

βœ” Validated input

βœ” UUID generation

βœ” Auto timestamps


5️⃣ SAM Template β€” What Each Part Does

  • Globals – shared settings for every Lambda: Python 3.13 runtime, 20s timeout, and env vars (TABLE_NAME, DDB_ENDPOINT) so code can read os.environ instead of hardcoding values.

  • TodoTable (DynamoDB) – a pay-per-request table named ${StackName}-todos with partition key id and a status-index GSI (Global Secondary Index). This is where all TODO items live.

  • HttpApi (API Gateway) – creates the /v1 HTTP API with permissive CORS so you can call it from anywhere during dev.

  • TodoFunction (Lambda)

    • CodeUri: src/, Handler: app.handler β†’ entry point is src/app.py::handler.
    • Policies: DynamoDBCrudPolicy β†’ grants this Lambda CRUD access to TodoTable.
    • Events wire HTTP verbs/paths to the single function:
    • POST /todos, GET /todos, GET /todos/{id}, PUT /todos/{id}, DELETE /todos/{id}
    • DDB_ENDPOINT is empty in AWS (uses managed DynamoDB). For local runs, set it in env_local.json to hit DynamoDB Local.
    • sam local start-api reads these mappings so local routing mirrors production.
  • Outputs – surface the API URL and table name after deploy so you can copy/paste them into tests or clients.


πŸƒβ€β™‚οΈ Running the Backend Locally

1️⃣ Start DynamoDB Local

docker compose up -d
Enter fullscreen mode Exit fullscreen mode

If you don’t have AWS creds configured, export dummy values so CLI + seed script work:

export AWS_ACCESS_KEY_ID=dummy
export AWS_SECRET_ACCESS_KEY=dummy
export AWS_REGION=ca-central-1
Enter fullscreen mode Exit fullscreen mode

2️⃣ Create & Activate Python Virtual Environment using venv

macOS / Linux:

python3 -m venv venv
source venv/bin/activate
pip install -r src/requirements.txt
Enter fullscreen mode Exit fullscreen mode

Windows (PowerShell):

python -m venv venv
.\venv\Scripts\Activate.ps1
pip install -r src/requirements.txt
Enter fullscreen mode Exit fullscreen mode

Deactivate any time:

deactivate
Enter fullscreen mode Exit fullscreen mode

3️⃣ Seed Data

export TABLE_NAME=local-todos
export DDB_ENDPOINT=http://localhost:8000
python3 scripts/seed_local_ddb.py
Enter fullscreen mode Exit fullscreen mode

4️⃣ Build the Project

sam build
Enter fullscreen mode Exit fullscreen mode

5️⃣ Create env_local.json

{
  "TodoFunction": {
    "TABLE_NAME": "local-todos",
    "DDB_ENDPOINT": "http://host.docker.internal:8000"
  }
}
Enter fullscreen mode Exit fullscreen mode

6️⃣ Start Local API Gateway

sam local start-api --env-vars env_local.json
# Add --debug to see detailed SAM logs while developing:
# sam local start-api --env-vars env_local.json --debug
Enter fullscreen mode Exit fullscreen mode

Endpoint:

http://127.0.0.1:3000
Enter fullscreen mode Exit fullscreen mode

7️⃣ Test Endpoints with curl (smoke test)

# Create
curl -sS -X POST http://127.0.0.1:3000/todos   -H 'Content-Type: application/json'   -d '{"title":"First Todo","description":"hello"}' | jq

# List
curl -sS http://127.0.0.1:3000/todos | jq

# Grab an id from the JSON above
ID="1a31c3fc-45b9-44f7-8152-47d258340d60"

# Get one
curl -sS http://127.0.0.1:3000/todos/$ID | jq

# Update
curl -sS -X PUT http://127.0.0.1:3000/todos/$ID   -H 'Content-Type: application/json'   -d '{"status":"done"}' | jq

# Delete
curl -i -sS -X DELETE http://127.0.0.1:3000/todos/$ID
Enter fullscreen mode Exit fullscreen mode

Cloud next steps (covered in Part 3): sam deploy --guided with a real DynamoDB table (leave DDB_ENDPOINT empty).


8️⃣ Stop Local Services

  • Stop SAM server: hit Ctrl+C in the terminal running sam local start-api.
  • Stop DynamoDB Local:
  docker compose down
Enter fullscreen mode Exit fullscreen mode

πŸ“š Additional Resources

GitHub Repository:

πŸ‘‰ aws-sam-gateway-lambda-dynamodb

AWS docs for deeper dives:


⏭️ Coming Up in Part 3

Part 3 covers:

  • Deploying to AWS with SAM
  • Deploying to AWS with CDK
  • S3 packaging
  • API Gateway
  • DynamoDB
  • AWS Free Tier vs Always-Free Notes
  • Budget Alerts to avoid costs

Stay tuned! πŸš€

Top comments (0)