Serverless development has become a go-to strategy for modern application architectures, and with good reason: it allows developers to focus more on building features and less on managing infrastructure. In this article, I walk you through building a fully serverless CRUD API using AWS SAM, Lambda, API Gateway, and DynamoDB all neatly wrapped in Python.
Whether you’re new to AWS SAM or just looking for a practical CRUD example to learn from, this guide has you covered.
Project Overview
The goal of this project was simple: create a clean, scalable, and serverless API capable of Create, Read, Update, and Delete (CRUD) operations on a DynamoDB table. The architecture leverages the following:
- API Gateway as the HTTP interface
- AWS Lambda as the compute layer (Python-powered)
- DynamoDB as the persistent data store
- AWS SAM (Serverless Application Model) for easy infrastructure deployment
One Lambda function handles all CRUD operations, reducing overhead and keeping the solution neat and maintainable.
1. Project Initialisation
Scaffold a new Python-based SAM project using:
sam init --runtime python3.12 --dependency-manager pip --app-template hello-world --name .
This generates the standard SAM directory structure, giving you a great starting point. Ensure that you initialise the SAM project in a new, empty subdirectory as sam init does not allow initialising a project in a non-empty directory.
2. One Lambda to Rule Them All
Rather than creating separate functions for each CRUD operation, I designed one Lambda function (core/app.py) to handle all actions. It inspects the HTTP path to determine which operation (/create, /read, /update, /delete) to execute.
Why?
- Less boilerplate
- Centralized logic
- Easier maintenance
Here is a code snippet of how I structured the 4 api endpoints in one lambda function
if http_method == 'POST':
try:
data = event.get('body', {})
while isinstance(data, str):
data = json.loads(data)
logger.info(f"Request Data (body): {data}")
match path:
case '/create':
return create(data)
case '/read':
return read(data)
case '/update':
return update(data)
case '/delete':
return delete(data)
case _:
return make_response(404, {'message': 'Path Not Found'})
3. API Gateway Configuration
Inside template.yaml,map the four CRUD routes to the single Lambda function:
Events:
CreateApi:
Type: Api
Properties:
Path: /create
Method: post
ReadApi:
Type: Api
Properties:
Path: /read
Method: post
UpdateApi:
Type: Api
Properties:
Path: /update
Method: post
DeleteApi:
Type: Api
Properties:
Path: /delete
Method: post
All these endpoints funnel into the same function, which processes the request and routes it accordingly.
find the rest of the sam template code at: „“
4. DynamoDB Integration
Provision a DynamoDB table named crud, using a simple primary key (id). Through boto3, AWS’s Python SDK, the Lambda function will perform the necessary CRUD operations on the table. As shown in the example below, the function performs a write to the DynamoDB table called ‚crud‘ and returns a response accordingly. This also applies to read, update and delete functions.
def create(data):
"""
Create a new item in the DynamoDB table.
"""
try:
dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('crud')
response = table.put_item(Item=data)
return make_response(200, {'message': 'Item created successfully'})
except Exception as e:
logger.error(f"Error creating item: {e}")
return make_response(500, {'message': 'Internal Server Error', 'error': str(e)})
5.Application level observability with aws-lambda-powertools & Cloudwatch
Structure logging and tracing using aws-lambda-powertools, this makes debugging easier and ensures operational visibility in CloudWatch. This was a small addition but made a big difference. Simply import and use the tools in your lambda code as shown below.
from aws_lambda_powertools import Logger, Tracer
logger = Logger()
tracer = Tracer()
How they complement cloud monitoring
Tool | Monitoring Type | Purpose |
---|---|---|
Logger | Application Logging (CloudWatch Logs) | Understand what happened and why. |
Tracer | Distributed Tracing (AWS X-Ray) | Understand how requests flow and where bottlenecks occur. |
logger in cloudwatch log groups:
6. Deployment with SAM
This is done in 3 steps. validate, build and deploy.
1. sam validate
This command checks your template.yaml (SAM template) for syntax errors and verifies that it’s a valid AWS SAM template.
Think of it like linting for your infrastructure it won’t deploy anything, just confirms your YAML and structure are correct.
2. sam build
This command packages your application code and dependencies, preparing them for deployment.
It:
- Creates a .aws-sam directory with the build artifacts.
- Resolves dependencies (like your Python packages from requirements.txt).
- Zips the code for Lambda functions as needed.
You run this before deploying to ensure everything is packaged correctly.
3. sam deploy –guided
This command deploys your serverless application to AWS.
- It uploads the artifacts created by sam build to S3.
- It creates or updates AWS resources defined in your template.yaml (API Gateway, Lambda, DynamoDB, etc.).
- –guided prompts you interactively for settings like stack name, region, and capabilities.
During your first deploy be keen on the prompts as the answers determine how your project runs.
Then a CloudFormation stack is created and you get prompted to allow the deployment of the stack. AWS SAM uses CloudFormation under the hood to manage resources.
After only a few minutes, your CRUD backend is up and running in AWS.
7. Testing the API
To validate functionality, I created test events mimicking API Gateway POST requests. These can be tested via the Lambda console or SAM CLI.
Here are payloads you can use to test the endpoints through lambda console:
Create (POST /create)
{
"httpMethod": "POST",
"path": "/create",
"body": {
"id": "123",
"attribute": "name",
"value": "John Doe"
}
}
And the successful lambda response:
Read (POST /read)
Now let’s retrieve what we created in the create test, using a read event test payload
{
"httpMethod": "POST",
"path": "/read",
"body": {
"id": "123"
}
}
lambda response:
Read (POST /update)
Now lets update John Doe to Jane Doe using an update event payload.
{
„httpMethod“: „POST“,
„path“: „/update“,
„body“: {
„id“: „123“,
„attribute“: „name“,
„value“: „Jane Doe“
}
}
lambda response:
The name was updated successfully in DynamoDB.
Delete (POST /delete)
Finally, let’s test the delete endpoint using a delete event payload.
{
"httpMethod": "POST",
"path": "/delete",
"body": {
"id": "123"
}
}
Our record with ID 123 was successfully deleted from DynamoDB.
Lessons Learned & Pro Tips
SAM is a Productivity Booster
It simplifies the complexity of packaging and deploying serverless applications.
Single Lambda for Multiple Endpoints = Simplicity
Keeping all logic in one handler makes routing clear and concise.
SAM Deployment Prompts Are Repetitive but Important
Sometimes SAM asks the same question twice — answer carefully, especially around authentication and capabilities.
Mind Your Dependencies
Ensure libraries like aws-lambda-powertools are in requirements.txt to avoid runtime issues.
I came across such errors in the lambda response.
To fix this, just add the dependency in the requirements.txt, then sam build and sam deploy, the process should be shorter and faster.
Happy coding!
Follow me for more demos and networking. Kevin Kiruri LinkedIn
Find the source code here: AWS SAM