AWS Builder Center

Building a Serverless REST API on AWS Using Lambda, API Gateway, and DynamoDB

Building a Serverless REST API on AWS Using Lambda, API Gateway, and DynamoDB

HAVE A GOOD DAY.
Building a Serverless REST API on AWS Using Lambda, API Gateway, and DynamoDB
Introduction
Modern applications need to be scalable, reliable, secure, and cost-effective. Traditionally, building a backend application required developers to manage servers, configure operating systems, maintain infrastructure, and plan for changing traffic demands. These responsibilities can increase operational complexity and slow down application development.
Serverless computing offers a different approach. Instead of managing servers directly, developers can focus on writing application code while AWS manages much of the underlying infrastructure.
In this article, we will explore how to build a serverless REST API using three essential AWS services:
- AWS Lambda: Runs application code without requiring developers to manage servers.
- Amazon API Gateway: Provides an HTTP interface through which clients can access backend functionality.
- Amazon DynamoDB: A managed NoSQL database designed for low-latency data access at scale.
We will build a simple task management API that allows users to create, retrieve, and delete tasks. Along the way, we will examine the architecture, implementation, security considerations, deployment process, and cost optimization strategies.
By the end of this guide, you will understand how these AWS services work together to create a practical serverless application.
1. Understanding Serverless Architecture
Serverless computing is a cloud computing model in which the cloud provider manages the infrastructure required to execute application workloads.
The term serverless does not mean that servers do not exist. Instead, it means developers do not need to provision, maintain, or scale the underlying servers themselves.
With AWS serverless services, developers can concentrate on application logic while AWS handles many infrastructure responsibilities.
Why choose serverless?
Serverless architecture offers several benefits:
1. Reduced operational overhead
Developers do not need to manage operating systems, apply routine server patches, or maintain a fleet of application servers.
2. Automatic scaling
AWS Lambda can execute multiple function invocations concurrently as demand changes, subject to account quotas and configured concurrency limits.
3. Pay-for-use pricing
Lambda charges are based on factors such as requests and execution duration. API Gateway and DynamoDB have their own pricing models, so the total cost depends on how each service is configured and used.
4. Faster development
Managed services reduce the amount of infrastructure code and operational work required to build an application.
5. Integration with AWS services
Serverless applications can integrate with services such as Amazon S3, Amazon SQS, Amazon EventBridge, and Amazon Cognito.
Serverless architecture is particularly useful for APIs, event-driven applications, automation tasks, data processing, and workloads with variable traffic.
2. Understanding the AWS Services
Before building the application, let's understand the role of each service.
AWS Lambda
AWS Lambda is a serverless compute service that executes code in response to events.
For our application, Lambda will process incoming API requests, validate the supplied data, and interact with DynamoDB.
For example, when a user creates a task, Lambda receives the request, generates a unique identifier, and saves the task in the database.
Lambda supports multiple programming languages and integrates with many AWS services.
Official documentation: https://docs.aws.amazon.com/lambda/
Amazon API Gateway
Amazon API Gateway allows developers to create and manage APIs that expose backend functionality to clients.
It acts as the HTTP entry point for our application.
We will configure endpoints such as:
- "POST /tasks" — Create a task.
- "GET /tasks/{id}" — Retrieve a task.
- "DELETE /tasks/{id}" — Delete a task.
API Gateway forwards incoming requests to Lambda and returns the function's response to the client.
It also supports features such as authorization, throttling, monitoring, and request validation, depending on the API type and configuration.
Official documentation: https://docs.aws.amazon.com/apigateway/
Amazon DynamoDB
Amazon DynamoDB is a fully managed NoSQL database that supports key-value and document data models.
It is suitable for applications requiring predictable, low-latency access to data at scale.
In our application, DynamoDB will store task records.
Each task will contain fields such as:
- Task ID
- Task title
- Task description
- Creation timestamp
We will use the task ID as the table's partition key.
Official documentation: https://docs.aws.amazon.com/amazondynamodb/
3. Designing the Application Architecture
Our application follows a straightforward request-processing flow.
Architecture flow:
Client → Amazon API Gateway → AWS Lambda → Amazon DynamoDB
Here is how the components interact.
Step 1: The client sends a request
A web application, mobile application, or API client sends an HTTP request to an API Gateway endpoint.
Step 2: API Gateway receives the request
API Gateway identifies the configured route and forwards the request to the appropriate Lambda function.
Step 3: Lambda processes the request
The function validates the request, determines the required operation, and prepares a database request.
Step 4: DynamoDB performs the database operation
DynamoDB creates, retrieves, or deletes the requested record.
Step 5: Lambda returns a response
The function converts the result into an HTTP-compatible response.
Step 6: API Gateway responds to the client
The client receives the status code and response body.
This architecture separates the API layer, application logic, and data storage. Each component has a specific responsibility, making the application easier to maintain and extend.
4. Defining the Task Management API
Let's define the endpoints before writing the implementation.
HTTP Method| Endpoint| Purpose
POST| "/tasks"| Create a new task
GET| "/tasks/{id}"| Retrieve a task by its ID
DELETE| "/tasks/{id}"| Delete a task by its ID
For simplicity, we will implement all three operations in one Lambda function.
A production application could separate these operations into multiple functions to support independent permissions, deployments, and scaling configurations.
Example request
To create a task, a client sends the following JSON payload:
{
"title": "Learn AWS Lambda",
"description": "Build a serverless application on AWS"
}
A successful response might look like this:
{
"message": "Task created successfully",
"task": {
"id": "generated-task-id",
"title": "Learn AWS Lambda",
"description": "Build a serverless application on AWS",
"createdAt": "2026-10-20T12:00:00Z"
}
}
The identifier and timestamp will be generated when the actual request is processed.
5. Creating the DynamoDB Table
The first implementation step is to create a DynamoDB table.
Open the AWS Management Console and navigate to Amazon DynamoDB.
Create a table with the following configuration:
- Table name: "Tasks"
- Partition key: "id"
- Partition key type: String
- Billing mode: On-demand, for a simple demonstration
On-demand capacity is convenient for a small learning project because you do not need to specify provisioned read and write capacity in advance.
However, on-demand billing is not automatically the cheapest option for every workload. Compare pricing and expected traffic before choosing a production configuration.
After creating the table, note its name and AWS Region.
The application will use the table to store records like this:
{
"id": "task-123",
"title": "Learn AWS Lambda",
"description": "Understand serverless computing",
"createdAt": "2026-10-20T12:00:00Z"
}
DynamoDB does not require a predefined schema for every non-key attribute. Records can contain different attributes, provided they follow the application's data model.
6. Creating the Lambda Function
Next, create a Lambda function to handle the API requests.
In the AWS Management Console:
1. Open AWS Lambda.
2. Choose Create function.
3. Select Author from scratch.
4. Enter "TaskApiFunction" as the function name.
5. Select a supported Python runtime.
6. Create or select an execution role.
7. Create the function.
For this example, we will use Python and the AWS SDK for Python, known as Boto3.
Configuring environment variables
Open the Lambda function's configuration and add this environment variable:
TASKS_TABLE = Tasks
Environment variables allow the function to read configuration without hardcoding the table name throughout the application.
For a production deployment, use an infrastructure-as-code template and a managed configuration strategy where appropriate.
Implementing the Lambda function
Replace the default function code with the following implementation:
import json
import os
import uuid
from datetime import datetime, timezone
import boto3
from botocore.exceptions import ClientError
dynamodb = boto3.resource("dynamodb")
table = dynamodb.Table(os.environ["TASKS_TABLE"])
def response(status_code, body):
return {
"statusCode": status_code,
"headers": {
"Content-Type": "application/json"
},
"body": json.dumps(body)
}
def lambda_handler(event, context):
method = event.get("httpMethod", "")
path_params = event.get("pathParameters") or {}
try:
if method == "POST":
body = json.loads(event.get("body") or "{}")
title = body.get("title")
description = body.get("description", "")
if not isinstance(title, str) or not title.strip():
return response(
400,
{"error": "A non-empty title is required"}
)
if not isinstance(description, str):
return response(
400,
{"error": "Description must be a string"}
)
task = {
"id": str(uuid.uuid4()),
"title": title.strip(),
"description": description,
"createdAt": datetime.now(
timezone.utc
).isoformat()
}
table.put_item(
Item=task,
ConditionExpression="attribute_not_exists(id)"
)
return response(
201,
{
"message": "Task created successfully",
"task": task
}
)
elif method == "GET":
task_id = path_params.get("id")
if not task_id:
return response(
400,
{"error": "Task ID is required"}
)
result = table.get_item(
Key={"id": task_id}
)
task = result.get("Item")
if not task:
return response(
404,
{"error": "Task not found"}
)
return response(200, {"task": task})
elif method == "DELETE":
task_id = path_params.get("id")
if not task_id:
return response(
400,
{"error": "Task ID is required"}
)
result = table.delete_item(
Key={"id": task_id},
ReturnValues="ALL_OLD"
)
deleted_task = result.get("Attributes")
if not deleted_task:
return response(
404,
{"error": "Task not found"}
)
return response(
200,
{"message": "Task deleted successfully"}
)
return response(
405,
{"error": "Method not allowed"}
)
except json.JSONDecodeError:
return response(
400,
{"error": "Invalid JSON request body"}
)
except ClientError:
print("DynamoDB operation failed")
return response(
500,
{"error": "Internal server error"}
)
How the code works
Let's examine the important parts.
1. Connecting to DynamoDB
dynamodb = boto3.resource("dynamodb")
table = dynamodb.Table(os.environ["TASKS_TABLE"])
Boto3 creates a DynamoDB resource and identifies the table using the environment variable.
The Lambda execution role must grant the required DynamoDB permissions for these operations.
2. Creating a task
task = {
"id": str(uuid.uuid4()),
"title": title.strip(),
"description": description,
"createdAt": datetime.now(timezone.utc).isoformat()
}
The application generates a unique identifier and records the creation timestamp.
The function then writes the task to DynamoDB.
3. Retrieving a task
result = table.get_item(
Key={"id": task_id}
)
The function uses the task ID to retrieve a record from DynamoDB.
If no record exists, the API returns an HTTP 404 response.
4. Deleting a task
result = table.delete_item(
Key={"id": task_id},
ReturnValues="ALL_OLD"
)
The function deletes the record and requests the previous item attributes.
This lets the application distinguish between an existing task and a task that was not found.
Important implementation note: This example uses the "httpMethod" event field associated with API Gateway's REST API payload format. HTTP APIs using payload format 2.0 use different event fields, including "requestContext.http.method" and "rawPath". If you select that configuration, adapt the handler accordingly.
The code is intended as a learning example, not a complete production application. Production deployments should also implement structured logging, appropriate error reporting, request size limits, comprehensive validation, and automated tests.
7. Configuring IAM Permissions
Security is one of the most important parts of building a serverless application.
AWS Identity and Access Management (IAM) determines which AWS resources a user, application, or service can access.
Lambda needs permission to perform specific operations on the "Tasks" table.
Create or update the Lambda execution role with a policy similar to this:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"dynamodb:PutItem",
"dynamodb:GetItem",
"dynamodb:DeleteItem"
],
"Resource": "arn:aws:dynamodb:REGION:ACCOUNT_ID:table/Tasks"
}
]
}
Replace "REGION" and "ACCOUNT_ID" with the appropriate values for your AWS environment.
This policy follows the principle of least privilege by allowing only the database operations required by this example.
Avoid granting broad permissions such as "dynamodb:*" or "AdministratorAccess" to the Lambda execution role.
Also ensure that the role includes the permissions required for CloudWatch Logs if you want the function to publish execution logs.
Official documentation: https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html
8. Connecting API Gateway to Lambda
Once the Lambda function is ready, configure API Gateway to expose the endpoints.
For this example, you can use an API Gateway REST API.
Step 1: Create an API
1. Open Amazon API Gateway.
2. Create a REST API.
3. Give the API a descriptive name, such as "TaskManagementAPI".
4. Create the API resources and methods.
Step 2: Create the "/tasks" resource
Create a resource named "tasks".
Configure the following method:
- HTTP method: "POST"
- Integration type: Lambda function
- Lambda function: "TaskApiFunction"
- Lambda proxy integration: Enabled
Lambda proxy integration passes the HTTP request information to the function through an event object.
Step 3: Create the "/tasks/{id}" resource
Under "/tasks", create a child resource named "{id}".
Configure these methods:
- "GET"
- "DELETE"
Connect both methods to the same Lambda function using Lambda proxy integration.
API Gateway will provide the path parameter through the "pathParameters" field.
Step 4: Deploy the API
Deploy the API to a stage, such as "dev".
API Gateway will provide an invocation URL.
Your endpoints will follow a pattern similar to:
https://API_ID.execute-api.REGION.amazonaws.com/dev/tasks
Use the generated URL from your own API Gateway deployment rather than copying the example literally.
Step 5: Test the integration
Before testing the API, confirm that API Gateway has permission to invoke the Lambda function. The console can usually configure this permission when you create the integration.
The Lambda execution role and API Gateway's invocation permission serve different purposes:
- Lambda execution role: Controls what the function can access.
- Lambda resource-based permission: Controls whether API Gateway can invoke the function.
Both must be configured correctly.
9. Testing the API
After deploying the API, test it using an HTTP client such as curl or an API testing application.
Creating a task
Run:
curl -X POST \
"https://API_ID.execute-api.REGION.amazonaws.com/dev/tasks" \
-H "Content-Type: application/json" \
-d '{
"title": "Learn Serverless AWS",
"description": "Build a REST API using Lambda and DynamoDB"
}'
Replace the example endpoint with your deployed API URL.
A successful request should return HTTP 201 and a JSON response containing the newly created task.
Retrieving a task
Copy the task ID from the creation response.
Then run:
curl \
"https://API_ID.execute-api.REGION.amazonaws.com/dev/tasks/TASK_ID"
Replace "TASK_ID" with the identifier returned by the previous request.
If the task exists, the API should return HTTP 200 and the task data.
Deleting a task
Run:
curl -X DELETE \
"https://API_ID.execute-api.REGION.amazonaws.com/dev/tasks/TASK_ID"
A successful deletion should return HTTP 200.
If the task does not exist, the function should return HTTP 404.
Expected results
Operation| Expected status| Result
Create a task| 201| Task created
Retrieve an existing task| 200| Task returned
Retrieve a missing task| 404| Task not found
Delete an existing task| 200| Task deleted
Delete a missing task| 404| Task not found
Submit invalid JSON| 400| Validation error
Use an unsupported HTTP method| 405| Method not allowed
These tests verify the primary API operations. Additional tests should cover malformed input, permission failures, database errors, oversized requests, and unexpected event formats.
10. Improving Security
A working API is only the beginning. A production application must also protect its data and control access to its endpoints.
Authentication and authorization
The example API does not implement authentication. Anyone who can reach its endpoints may be able to create, retrieve, or delete tasks.
Do not expose this implementation as a public production API without adding appropriate access controls.
Depending on the application, consider:
- Amazon Cognito for user authentication.
- API Gateway authorizers for access control.
- IAM authorization for suitable service-to-service access patterns.
- A Lambda authorizer for custom authorization logic.
Authentication establishes who the caller is. Authorization determines what that caller is permitted to do.
For a multi-user task management application, each task should also be associated with its owner. The backend must verify ownership before returning or deleting a task.
Encrypting data
DynamoDB encrypts stored data at rest. AWS services also support encrypted communication in transit through HTTPS and TLS.
Use HTTPS for API requests and consider whether your application requires customer-managed encryption keys or additional controls.
Protecting sensitive information
Avoid logging passwords, access tokens, secret keys, or sensitive personal data
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