
Building a Serverless Backend for an AI Application Using AWS Lambda, API Gateway, and DynamoDB
In the previous article, we introduced our AI-powered University Knowledge Assistant and explored its architecture, use case, and AWS technology stack. Now it's time to start building. Every AI application needs a backend to receive user requests, validate input, manage application data, and coordinate calls to AI services. For our project, we'll use a serverless architecture powered by AWS Lambda, Amazon API Gateway, and Amazon DynamoDB.
Series: Generative AI on AWS (3 articles)
- 2Building a Serverless Backend for an AI Application Using AWS Lambda, API Gateway, and DynamoDB This article

Part 2 of the Generative AI on AWS series
Introduction
In the previous article, we introduced our AI-powered University Knowledge Assistant and explored its architecture, use case, and AWS technology stack.
Now it's time to start building.
Every AI application needs a backend to receive user requests, validate input, manage application data, and coordinate calls to AI services. For our project, we'll use a serverless architecture powered by AWS Lambda, Amazon API Gateway, and Amazon DynamoDB.
Serverless computing allows us to run application code without provisioning or managing traditional servers. AWS manages the underlying infrastructure, while we focus on application logic, configuration, and security.
In this article, we'll:
- Understand the role of each AWS service.
- Design a serverless backend architecture.
- Create an AWS Lambda function using Python.
- Expose HTTP endpoints through Amazon API Gateway.
- Store application metadata in DynamoDB.
- Test the API and review security and cost considerations.
- Prepare the backend for Amazon Bedrock integration in the next article.
By the end, we'll have a basic serverless API that can serve as the foundation for our University Knowledge Assistant.
1. Why Choose a Serverless Architecture?
When building a backend, developers commonly choose between traditional server-based infrastructure, containers, and serverless computing.
For a project like our University Knowledge Assistant, serverless offers several advantages.
No server management
AWS Lambda runs our code without requiring us to provision, patch, or maintain application servers.
Automatic scaling
Lambda can scale the number of concurrent executions in response to incoming requests, subject to configured concurrency limits and service quotas.
Pay-per-use pricing
Lambda charges are based primarily on requests and execution duration, with a free tier subject to current AWS eligibility and limits. Other services, including API Gateway and DynamoDB, have their own pricing models.
Integration with AWS services
Lambda integrates with API Gateway, DynamoDB, S3, Amazon Bedrock, CloudWatch, and other AWS services.
Faster prototyping
We can focus on implementing application functionality rather than setting up and maintaining a complete server environment.
However, serverless isn't always the right choice. Applications with long-running workloads, complex persistent connections, or specialized runtime requirements may benefit from containers or conventional servers. For this initial project, serverless is a suitable approach to explore.
2. Understanding the AWS Services
Before implementing the backend, let's understand the three main services.
AWS Lambda
AWS Lambda is a serverless compute service that executes code in response to events.
For our project, Lambda will handle incoming API requests, validate user input, coordinate application logic, and eventually invoke the AI retrieval workflow.
We'll use Python for the initial implementation.
Amazon API Gateway
Amazon API Gateway provides a managed way to create and expose HTTP APIs.
It acts as the entry point for our backend, receiving requests from the frontend and invoking the corresponding Lambda function.
It also supports features such as authorization, throttling, request validation, and monitoring, depending on the API type and configuration.
Amazon DynamoDB
Amazon DynamoDB is a fully managed NoSQL database.
We'll use it to store structured application metadata, such as conversation records and request identifiers. In a production system, the data model will need to account for access patterns, retention requirements, and user authorization.
For this tutorial, we'll create a simple table to store conversation records.
3. Designing the Serverless Architecture
Our backend will follow this request flow:
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
STUDENT
|
v
+------------------+
| React Frontend |
+------------------+
|
| HTTPS Request
v
+------------------+
| API Gateway |
| HTTP API |
+------------------+
|
v
+------------------+
| AWS Lambda |
| Python Handler |
+------------------+
| |
| |
v v
+-----------+ +----------------+
| DynamoDB | | Application |
| | | Logic |
+-----------+ +----------------+
|
v
+----------------+
| Amazon Bedrock|
| (Next Article)|
+----------------+
Supporting services:
IAM | CloudWatch | AWS BudgetsFigure 1: Serverless backend architecture for the University Knowledge Assistant.
The frontend sends an HTTP request to API Gateway. API Gateway invokes our Lambda function, which processes the request and interacts with DynamoDB as needed.
In this article, we'll implement a basic chat endpoint that stores the student's question and returns a temporary placeholder response. In the next article, we'll replace that placeholder with actual retrieval and generation using Amazon Bedrock.
This separation allows us to develop and test the backend independently of the AI integration.
4. Defining the API
We'll start with two endpoints.
| Method | Endpoint | Purpose |
|---|---|---|
| GET | /health | Check whether the API is running |
| POST | /chat | Submit a question and receive a response |
The chat endpoint will accept a JSON request containing a question and an optional conversation identifier.
Example request:
1
2
3
4
{
"question": "What is the attendance requirement?",
"conversation_id": "conv-001"
}Example response:
1
2
3
4
5
6
{
"conversation_id": "conv-001",
"question": "What is the attendance requirement?",
"answer": "The AI-powered response will be integrated in the next article.",
"status": "prototype"
}The conversation identifier allows us to associate multiple requests with the same conversation. For this initial prototype, we'll accept it as a request field; a production application should derive user identity from a verified authentication mechanism rather than trusting arbitrary client-supplied identifiers.
5. Creating the DynamoDB Table
First, we'll create a DynamoDB table to store conversation records.
Open the AWS Management Console and navigate to Amazon DynamoDB.
Choose Create table and use the following configuration:
| Setting | Value |
|---|---|
| Table name | UniversityAssistantConversations |
| Partition key | conversation_id |
| Partition key type | String |
| Billing mode | On-demand |
For a simple prototype, a single partition key is sufficient to demonstrate storing and retrieving conversation records.
For a production application, we would revisit this schema to support multiple messages per conversation, user-level access patterns, pagination, retention, and efficient querying.
Creating the table with AWS CLI
You can also create the table using the AWS CLI:
1
2
3
4
5
6
7
aws dynamodb create-table \
--table-name UniversityAssistantConversations \
--attribute-definitions \
AttributeName=conversation_id,AttributeType=S \
--key-schema \
AttributeName=conversation_id,KeyType=HASH \
--billing-mode PAY_PER_REQUESTThis command assumes that the AWS CLI is installed and configured with credentials and a default region that supports DynamoDB.
To verify the table:
1
2
aws dynamodb describe-table \
--table-name UniversityAssistantConversationsWait until the table is active before proceeding.
6. Implementing the AWS Lambda Function
Now let's create our Python backend.
We'll use a single Lambda handler for the initial implementation, with basic request validation and DynamoDB integration.
Project structure
1
2
3
4
5
6
7
8
9
10
university-knowledge-assistant/
│
├── backend/
│ ├── lambda_function.py
│ └── requirements.txt
│
├── frontend/
│ └── ...
│
└── README.mdCreate
lambda_function.py inside the backend directory.Python implementation
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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
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["CONVERSATIONS_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("requestContext", {}) \
.get("http", {}) \
.get("method", "GET")
path = event.get("rawPath", "/")
if method == "GET" and path == "/health":
return response(200, {
"status": "healthy",
"service": "university-knowledge-assistant"
})
if method == "POST" and path == "/chat":
return handle_chat(event)
return response(404, {
"error": "Route not found"
})
def handle_chat(event):
try:
body = json.loads(event.get("body") or "{}")
except (json.JSONDecodeError, TypeError):
return response(400, {
"error": "Invalid JSON request body"
})
question = body.get("question")
if not isinstance(question, str) or not question.strip():
return response(400, {
"error": "A non-empty question is required"
})
question = question.strip()
if len(question) > 2000:
return response(400, {
"error": "Question exceeds the maximum length"
})
conversation_id = body.get("conversation_id")
if not isinstance(conversation_id, str) or not conversation_id:
conversation_id = str(uuid.uuid4())
timestamp = datetime.now(
timezone.utc
).isoformat()
# Placeholder until Bedrock is integrated.
answer = (
"The AI-powered response will be "
"integrated in the next article."
)
try:
table.put_item(
Item={
"conversation_id": conversation_id,
"updated_at": timestamp,
"last_question": question,
"status": "prototype"
}
)
except ClientError:
return response(500, {
"error": "Unable to save conversation"
})
return response(200, {
"conversation_id": conversation_id,
"question": question,
"answer": answer,
"status": "prototype"
})This implementation demonstrates the core request-handling flow. It validates the request, creates or accepts a conversation identifier, stores a basic record in DynamoDB, and returns a placeholder response.
A few important details:
- The DynamoDB table name is read from an environment variable rather than hardcoded.
- Invalid JSON and empty questions are rejected.
- The question length is limited to prevent excessively large requests.
- The database operation is wrapped in error handling.
- The response is deliberately a placeholder because the AI integration comes in the next article.
For a production implementation, we'd add authentication, authorization, structured logging, request identifiers, rate limiting, stricter validation, and a more complete conversation data model. We'd also ensure that a client cannot access another user's conversation by guessing its identifier.
7. Configuring the Lambda Function
Navigate to the AWS Lambda console and select Create function.
Choose Author from scratch and configure the function:
| Setting | Value |
|---|---|
| Function name | UniversityAssistantAPI |
| Runtime | Python 3.12, if available in your region |
| Architecture | x86_64 |
| Execution role | Create a new role with basic Lambda permissions |
Select Create function.
Paste the Python code into the inline editor and save the function.
Configure the environment variable
In the Lambda configuration, navigate to Environment variables and add:
| Key | Value |
|---|---|
| CONVERSATIONS_TABLE | UniversityAssistantConversations |
This allows the function to locate the DynamoDB table without embedding the table name in the application logic.
Configure DynamoDB permissions
The Lambda execution role needs permission to write to the DynamoDB table.
For this prototype, attach a narrowly scoped IAM policy granting
dynamodb:PutItem on the specific table ARN.Example policy:
1
2
3
4
5
6
7
8
9
10
11
12
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"dynamodb:PutItem"
],
"Resource": "arn:aws:dynamodb:REGION:ACCOUNT_ID:table/UniversityAssistantConversations"
}
]
}Replace
REGION and ACCOUNT_ID with your actual AWS region and account ID.The role should retain the basic Lambda execution permissions needed for CloudWatch logging. Avoid granting broad DynamoDB or administrator access to the function.
8. Creating an HTTP API with Amazon API Gateway
With the Lambda function configured, we'll expose it through Amazon API Gateway.
Navigate to the API Gateway console and choose Create API.
Select HTTP API, then configure a Lambda integration.
Choose the
UniversityAssistantAPI function and create the API.Configure routes
Create the following routes:
| Method | Route | Integration |
|---|---|---|
| GET | /health | UniversityAssistantAPI |
| POST | /chat | UniversityAssistantAPI |
For this initial prototype, both routes can invoke the same Lambda function, which dispatches requests based on the HTTP method and path.
Deploy the API to a stage, such as
dev, and note the generated API endpoint URL.The resulting endpoint will resemble:
1
https://<api-id>.execute-api.<region>.amazonaws.comYour actual API ID and region will differ.
Configure CORS
If your React frontend is hosted on a different origin, configure Cross-Origin Resource Sharing (CORS) in API Gateway.
For development, allow the specific local frontend origin, such as:
1
http://localhost:3000Configure only the required methods and headers. For production, replace the development origin with your deployed frontend's exact origin.
CORS is a browser access-control mechanism, not an authentication or authorization system. The API still needs appropriate access controls.
9. Testing the API
Now we can test our backend independently of the frontend.
Test the health endpoint
1
2
curl \
https://<api-id>.execute-api.<region>.amazonaws.com/healthExpected response:
1
2
3
4
{
"status": "healthy",
"service": "university-knowledge-assistant"
}Test the chat endpoint
1
2
3
4
5
6
curl -X POST \
https://<api-id>.execute-api.<region>.amazonaws.com/chat \
-H "Content-Type: application/json" \
-d '{
"question": "What is the attendance requirement?"
}'Expected response:
1
2
3
4
5
6
{
"conversation_id": "generated-conversation-id",
"question": "What is the attendance requirement?",
"answer": "The AI-powered response will be integrated in the next article.",
"status": "prototype"
}The generated conversation ID will be different for each new conversation.
Verify the DynamoDB record
Open the DynamoDB console and navigate to
UniversityAssistantConversations.Choose Explore table items and verify that a record was created with the conversation ID, timestamp, last question, and prototype status.
If the API returns an error, inspect the Lambda logs in Amazon CloudWatch. Common issues include:
- Incorrect DynamoDB table name in the environment variable.
- Missing DynamoDB permissions on the Lambda execution role.
- Incorrect API Gateway route or integration configuration.
- A Lambda runtime or deployment configuration issue.
At this stage, the API should be able to receive a question, persist basic conversation metadata, and return a structured response.
10. Connecting the Frontend
Our React frontend will send requests to the API Gateway endpoint.
A simple JavaScript example:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
async function askQuestion(question) {
const response = await fetch(
`${API_BASE_URL}/chat`,
{
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
question: question
})
}
);
if (!response.ok) {
throw new Error("Unable to process the question");
}
return await response.json();
}Here,
API_BASE_URL should be configured using the frontend's environment configuration.The UI can display the returned answer and conversation identifier. When the AI integration is implemented, the frontend contract can be extended to display source documents, citations, loading states, and errors.
Never embed AWS access keys or privileged credentials in browser-side code. The frontend should communicate with the API, and the backend should access AWS services using its execution role.
11. Security Considerations
Even a prototype should follow basic cloud security practices.
IAM least privilege
Give the Lambda execution role only the permissions it needs. Our initial function needs to write to one DynamoDB table, not administer the entire database.
Authentication and authorization
For a production deployment, integrate Amazon Cognito or another trusted identity provider. Protect the chat and administrative endpoints, and enforce access to conversation records on the backend.
Input validation
Validate incoming JSON, limit question length, and reject malformed requests. Additional controls should include rate limits and request-size restrictions.
Data protection
Use HTTPS for API communication and encryption at rest for stored data. Avoid storing unnecessary personal information in DynamoDB or application logs.
Monitoring
Use CloudWatch to inspect errors, request volume, and Lambda execution metrics. Configure appropriate alarms for sustained failures or unusual traffic.
12. Understanding the Costs
A serverless architecture can reduce infrastructure management and make costs more closely reflect actual usage, but it is not automatically free.
Potential cost components include:
- API Gateway HTTP API requests.
- Lambda requests and execution duration.
- DynamoDB read and write operations, storage, and applicable features.
- CloudWatch logs, metrics, and alarms.
- Data transfer and any additional services used.
The actual cost depends on region, request volume, function memory and duration, database configuration, log retention, and current AWS pricing.
For an educational prototype, use the AWS Pricing Calculator to estimate costs, review the AWS Free Tier eligibility applicable to your account, and configure AWS Budgets to monitor spending. Avoid leaving unnecessary resources running.
13. What We've Built
In this article, we designed and implemented the serverless foundation for our University Knowledge Assistant.
We've:
- Created a Python-based Lambda function.
- Exposed HTTP endpoints using API Gateway.
- Created a DynamoDB table for basic conversation metadata.
- Configured environment variables and scoped IAM permissions.
- Implemented request validation and error handling.
- Tested the health and chat endpoints.
- Reviewed security, monitoring, and cost considerations.
Our backend currently returns a placeholder response. The next milestone is to connect the application to an actual knowledge base and foundation model.
Conclusion
Serverless architecture provides a practical foundation for developing cloud-native applications. With AWS Lambda, API Gateway, and DynamoDB, we can create a modular backend without managing traditional servers.
However, the real value of this architecture comes from integrating it with the application’s core functionality. For our University Knowledge Assistant, that means connecting the backend to a document retrieval pipeline and a generative AI model.
In the next article, we'll explore Amazon Bedrock Knowledge Bases and build the RAG pipeline that transforms our basic API into a document-grounded AI assistant. We'll configure document storage, ingest academic documents, retrieve relevant information, and generate answers using a foundation model.
Next: Building a RAG Application with Amazon Bedrock Knowledge Bases and Amazon S3.
Series: Generative AI on AWS (3 articles)
- 2Building a Serverless Backend for an AI Application Using AWS Lambda, API Gateway, and DynamoDB This article
Enjoyed reading this content? Let the author know!
Your likes, comments, shares, and saves help creators reach more builders.
Loading recommendations
Loading article