AWS Builder Center

Build a Serverless REST API on AWS in 30 Minutes with Lambda, API Gateway, and DynamoDB

A hands-on walkthrough of building, securing, and testing a pay-per-request REST API with zero servers to manage. You'll get working Python code, a least-privilege IAM policy, and the cost and cleanup tips most tutorials skip.

Exploring Things
Most "hello world" tutorials stop right before the interesting part. In this guide, you'll build a small but real REST API that stores and retrieves data, uses least-privilege permissions, and costs close to nothing when idle.
By the end you'll have:
  • POST /items to create an item
  • GET /items/{id} to fetch an item
  • No servers, no patching, and no capacity planning

Architecture

1
Client → API Gateway (HTTP API) → Lambda (Python) → DynamoDB
ServiceRole
API Gateway (HTTP API)Public HTTPS front door and routing
AWS LambdaRuns your code on demand
Amazon DynamoDBServerless key-value / document storage
Why an HTTP API instead of a REST API? HTTP APIs are cheaper and lower latency, and they cover most CRUD use cases. Choose the REST API type only if you need features like usage plans or request validation at the gateway.

Prerequisites

  • An AWS account (the free tier covers everything here)
  • Basic Python knowledge
  • 30 minutes

Step 1: Create the DynamoDB table

  1. Open the DynamoDB console and choose Create table.
  2. Table name: items
  3. Partition key: id (String)
  4. Under table settings, choose Customize settings, then On-demand capacity.
  5. Choose Create table.
On-demand mode means you pay per request and never have to guess capacity.

Step 2: Create the Lambda function

  1. Open the Lambda console and choose Create function.
  2. Choose Author from scratch, name it items-api, and select the Python 3.13 runtime (or the latest available).
  3. Under Configuration → Environment variables, add TABLE_NAME = items.
  4. Replace the code with:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import json
import os
import uuid

import boto3

table = boto3.resource("dynamodb").Table(os.environ["TABLE_NAME"])


def response(status, body):
return {
"statusCode": status,
"headers": {"Content-Type": "application/json"},
"body": json.dumps(body),
}


def lambda_handler(event, context):
route = event["routeKey"]

try:
if route == "POST /items":
data = json.loads(event.get("body") or "{}")
if "name" not in data:
return response(400, {"message": "'name' is required"})

item = {"id": str(uuid.uuid4()), "name": data["name"]}
table.put_item(Item=item)
return response(201, item)

if route == "GET /items/{id}":
item_id = event["pathParameters"]["id"]
result = table.get_item(Key={"id": item_id})
item = result.get("Item")
if not item:
return response(404, {"message": "Item not found"})
return response(200, item)

return response(404, {"message": "Route not found"})

except json.JSONDecodeError:
return response(400, {"message": "Invalid JSON body"})
except Exception as exc:
print(f"Unhandled error: {exc}") # goes to CloudWatch Logs
return response(500, {"message": "Internal server error"})
  1. Choose Deploy.

Step 3: Grant least-privilege permissions

Your function can't touch DynamoDB yet. Open Configuration → Permissions, choose the execution role, and add this inline policy. Replace the Region and account ID with yours:
1
2
3
4
5
6
7
8
9
10
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["dynamodb:PutItem", "dynamodb:GetItem"],
"Resource": "arn:aws:dynamodb:REGION:ACCOUNT_ID:table/items"
}
]
}
Resist the urge to attach AmazonDynamoDBFullAccess. Scoping to two actions on one table means a bug or a leaked credential can't touch anything else.

Step 4: Create the HTTP API

  1. Open API Gateway and choose Create API → HTTP API → Build.
  2. Add an integration: Lambda, and select items-api.
  3. Add two routes pointing to that integration:
    • POST /items
    • GET /items/{id}
  4. Keep the default stage ($default) with auto-deploy enabled.
  5. Create the API and copy the Invoke URL.

Step 5: Test it

Create an item:
1
2
3
curl -X POST https://YOUR_API_ID.execute-api.REGION.amazonaws.com/items \
-H "Content-Type: application/json" \
-d '{"name": "My first serverless item"}'
Response:
1
{"id": "3f2b8c1e-...", "name": "My first serverless item"}
Fetch it back with the returned ID:
1
curl https://YOUR_API_ID.execute-api.REGION.amazonaws.com/items/3f2b8c1e-...
If something fails, check CloudWatch Logs for the items-api function. That's where your print output and stack traces land.

What does this cost?

At low traffic, almost nothing. You pay for:
  • API Gateway: per million requests
  • Lambda: per request plus compute time (with a generous monthly free tier)
  • DynamoDB: per read/write request on on-demand tables
Check current pricing on each service's pricing page, since rates vary by Region and change over time. An idle API costs essentially nothing, which is the biggest advantage over an always-on server.

Production hardening checklist

This is a solid foundation, but before real users arrive, consider:
  • [ ] Authentication: add a JWT authorizer (for example with Amazon Cognito) on your routes
  • [ ] Throttling: set rate limits on the stage to protect against abuse and surprise bills
  • [ ] Infrastructure as code: recreate this with AWS SAM, CDK, or Terraform so it's repeatable
  • [ ] Observability: enable structured logging and set a CloudWatch alarm on Lambda errors
  • [ ] Input validation: validate field types and lengths, not just presence
  • [ ] Backups: enable point-in-time recovery on the DynamoDB table

Clean up

To avoid leftover resources, delete the HTTP API, the Lambda function (and its log group), and the DynamoDB table.

Wrap-up

In under 100 lines of code, you built an API that scales from zero to thousands of requests without you managing a single server. From here, try adding PUT and DELETE routes, or put a frontend in front of it.
If you build on this, I'd love to hear what you make. Share it in the comments below.
Any opinions in this article are those of the individual author and may not reflect the opinions of AWS.
Enjoyed reading this content? Let the author know!

Your likes, comments, shares, and saves help creators reach more builders.

Loading recommendations

Loading article