When I first started learning AWS, I thought deploying a backend meant creating an EC2 instance, installing Node.js, configuring a web server, and keeping everything running.
That approach works. But what if you could build an API without managing the underlying servers yourself?
That is where serverless architecture comes in.
In this article, I’ll explore three AWS services that work well together to build serverless backends:
- Amazon API Gateway — receives and manages API requests.
- AWS Lambda — executes backend code.
- Amazon DynamoDB — stores and retrieves data.
We’ll also look at how these services connect, explore a small Node.js example, and discuss when serverless makes sense in a real application.
1. What Does Serverless Actually Mean?
Serverless does not mean there are no servers.
Servers still exist. The difference is that AWS manages much of the underlying infrastructure, including server provisioning and infrastructure availability for these services.
Instead of managing a server yourself, you focus more on writing application code and defining how the services should work together.
For example, imagine you are building a task management application.
Users should be able to:
- Create a task.
- View their tasks.
- Update a task.
- Delete a task.
With a traditional backend, you might deploy a Node.js application on EC2 or ECS.
With a serverless approach, you could use API Gateway, Lambda, and DynamoDB.
The basic flow looks like this:
User / React Frontend
|
v
API Gateway
|
v
Lambda
|
v
DynamoDB
|
v
JSON Response
Each service has a specific responsibility. This separation makes the architecture easier to understand and lets you scale different parts of the application according to their needs.
2. Amazon API Gateway — The Entry Point for Your API
Imagine your React frontend needs to request a list of tasks from your backend.
It might send a request such as:
GET /tasks
But where should that request go?
Amazon API Gateway provides an API entry point that clients can call. It can route requests to backend services such as Lambda functions.
It also supports capabilities such as authorization, request validation, throttling, and monitoring integrations, depending on the API type and configuration.
For example, you might define these routes:
| HTTP Method | Endpoint | Purpose |
|---|---|---|
| GET | /tasks |
Retrieve tasks |
| GET | /tasks/{id} |
Retrieve one task |
| POST | /tasks |
Create a task |
| PUT | /tasks/{id} |
Update a task |
| DELETE | /tasks/{id} |
Delete a task |
Each route can be connected to a suitable Lambda function.
For example:
GET /tasks
|
v
getTasks Lambda
POST /tasks
|
v
createTask Lambda
API Gateway can also help protect an API from excessive requests through throttling and integrate with supported authorization mechanisms.
One important point: API Gateway is not your database, and it does not automatically make every API secure. Authentication, authorization, validation, and appropriate access policies still need to be configured.
3. AWS Lambda — Run Backend Code Without Managing Servers
Now let's look at the service that executes our backend logic.
AWS Lambda runs code in response to events. An API Gateway request can trigger a Lambda function, which processes the request and returns a response.
You don't need to manage an EC2 instance just to keep this function available.
For example, a createTask Lambda function might:
- Receive the incoming request.
- Read the task details.
- Validate the input.
- Store the task in DynamoDB.
- Return a response to the client.
A simplified flow looks like this:
API Gateway
|
v
Lambda Function
|
+--> Validate request
|
+--> Execute business logic
|
+--> Save data in DynamoDB
|
v
Return JSON response
Lambda supports multiple runtimes, including Node.js and Python.
A simple Node.js Lambda example
Here is a minimal Lambda handler that returns a JSON response.
export const handler = async (event) => {
return {
statusCode: 200,
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
message: "Hello from AWS Lambda!",
method: event.requestContext?.http?.method,
}),
};
};
This example assumes an API Gateway HTTP API using its payload format that exposes the HTTP method under requestContext.http.method.
When API Gateway invokes the function, Lambda returns a response that API Gateway can send back to the client.
This is only a demonstration. A production API would also need suitable error handling, input validation, authentication, and logging.
What happens when traffic increases?
Suppose your application normally receives 20 requests per minute, but suddenly receives thousands.
Lambda can scale the number of concurrent function executions in response to incoming demand, subject to account quotas, concurrency settings, and other service limits.
You don't manually create more EC2 instances just to handle every increase in traffic.
However, scaling is not unlimited or instantaneous in every situation. Concurrency quotas, downstream database capacity, throttling, and cold starts can affect performance.
That's why serverless applications still require thoughtful design.
4. Amazon DynamoDB — Store Application Data
Our backend now has an entry point and a way to execute code. But where should the application store its data?
For this example, we'll use Amazon DynamoDB.
DynamoDB is a managed NoSQL database service that supports key-value and document data models.
Unlike a traditional relational database, DynamoDB does not require you to design every relationship around SQL tables and joins. Instead, you design tables and indexes around the access patterns your application needs.
Imagine storing tasks with fields such as:
{
"userId": "user-123",
"taskId": "task-001",
"title": "Learn AWS Lambda",
"completed": false
}
A DynamoDB table could use:
-
userIdas the partition key. -
taskIdas the sort key.
This key design makes it possible to retrieve tasks belonging to a particular user using a query on that user's partition key.
The actual design should depend on the application's access patterns. If you need to search tasks by status, due date, or another attribute, you may need additional indexes or a different key design.
Why choose DynamoDB?
Some useful characteristics include:
- Managed infrastructure.
- Flexible scaling options.
- Low-latency access for suitable key-based access patterns.
- Integration with Lambda and other AWS services.
- On-demand capacity options for workloads where usage is difficult to predict.
But DynamoDB is not automatically the best database for every application.
If your application relies heavily on relational joins, complex SQL queries, or relational constraints, Amazon RDS or Amazon Aurora may be more suitable.
The important lesson is to choose a database based on your data model and access patterns, not simply because it is serverless.
5. Connecting the Architecture
Let's put the three services together.
Architecture overview: the frontend sends an API request, API Gateway routes it to Lambda, and Lambda interacts with DynamoDB before returning a response.
Here is the same architecture as a text diagram:
USER
|
v
REACT FRONTEND
|
HTTPS Request
|
v
AMAZON API GATEWAY
|
Route to Lambda
|
v
AWS LAMBDA
|
Validate and Process
|
v
AMAZON DYNAMODB
|
Query / Write
|
v
AWS LAMBDA
|
JSON Response
|
v
API GATEWAY
|
v
REACT FRONTEND
Let's walk through a request to create a task.
- The user fills out a form in the React application.
- The frontend sends a
POST /tasksrequest. - API Gateway receives the request and invokes the configured Lambda function.
- Lambda validates the input and executes the business logic.
- Lambda writes the task to DynamoDB using the AWS SDK.
- Lambda returns a response.
- API Gateway sends the response to the frontend.
- React updates the interface to show the result.
This architecture separates the frontend, API entry point, application logic, and database responsibilities.
For a small application, it can be a straightforward way to build and deploy a backend. As the application grows, you'll need to think more carefully about permissions, observability, data access patterns, and failure handling.
6. A Practical Node.js Example: Saving a Task to DynamoDB
Let's make the example a little more realistic.
Suppose we have a DynamoDB table named Tasks with userId as its partition key and taskId as its sort key.
The Lambda function below creates a task using the AWS SDK for JavaScript v3.
Install the required package in your project:
npm install @aws-sdk/client-dynamodb @aws-sdk/lib-dynamodb
Create a file named index.mjs:
import { randomUUID } from "node:crypto";
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import {
DynamoDBDocumentClient,
PutCommand,
} from "@aws-sdk/lib-dynamodb";
const client = new DynamoDBClient({});
const dynamoDB = DynamoDBDocumentClient.from(client);
const TABLE_NAME = process.env.TABLE_NAME;
export const handler = async (event) => {
try {
if (!TABLE_NAME) {
throw new Error("TABLE_NAME is not configured");
}
const body = JSON.parse(event.body || "{}");
const { userId, title } = body;
if (
typeof userId !== "string" ||
!userId.trim() ||
typeof title !== "string" ||
!title.trim()
) {
return {
statusCode: 400,
body: JSON.stringify({
message: "userId and title are required",
}),
};
}
const task = {
userId: userId.trim(),
taskId: randomUUID(),
title: title.trim(),
completed: false,
createdAt: new Date().toISOString(),
};
await dynamoDB.send(
new PutCommand({
TableName: TABLE_NAME,
Item: task,
ConditionExpression:
"attribute_not_exists(userId) AND attribute_not_exists(taskId)",
})
);
return {
statusCode: 201,
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(task),
};
} catch (error) {
console.error("Failed to create task:", error);
return {
statusCode: 500,
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
message: "Unable to create task",
}),
};
}
};
This example demonstrates the basic flow, but there are a few important details to understand.
First, the table name comes from an environment variable. Configure TABLE_NAME in the Lambda function's environment settings rather than hardcoding the table name throughout your code.
Second, the function uses the AWS SDK's default credential provider chain. When deployed to Lambda, it can obtain temporary credentials from the function's execution role. You generally should not put permanent AWS access keys in your application code.
Third, the conditional expression prevents overwriting an item with the same composite primary key. Because this example generates a UUID for each new task, a collision is extremely unlikely, but the condition still makes the intended behavior explicit.
Fourth, the example is simplified. It accepts a userId from the request body for demonstration. In a real multi-user application, derive the user's identity from a verified authentication mechanism rather than trusting a client-supplied user ID. Otherwise, a user might attempt to create or access another user's data.
Also, the catch block returns a generic error to the client while logging the underlying error for troubleshooting. Production applications should distinguish expected client errors from unexpected server failures and avoid exposing internal details.
Important: configure IAM permissions
The Lambda execution role needs permission to write to the intended DynamoDB table.
A tightly scoped policy should grant only the required action, such as dynamodb:PutItem, on the specific table ARN. If the function later needs to query or update tasks, grant only those additional actions.
Avoid giving the function broad permissions such as dynamodb:* on every table in the account.
Important: configure the DynamoDB table
The table must exist before this function can use it. Its primary key must match the attributes in the item:
- Partition key:
userId(String). - Sort key:
taskId(String).
Configure the TABLE_NAME environment variable with the table's actual name.
This code is a handler example, not a complete deployment tutorial. You still need to create the table, configure the Lambda runtime and execution role, connect the function to API Gateway, and test the request and response formats.
7. What About Authentication and Security?
A serverless backend still needs security. AWS managing the infrastructure does not automatically secure your application's business logic or data.
Here are some services and controls that may be useful.
AWS IAM
IAM controls which AWS resources and actions an identity can access.
For this architecture, the Lambda execution role should grant only the permissions the function needs, such as writing to one DynamoDB table.
Amazon Cognito or another authentication provider
If users need to sign in, you need an authentication mechanism.
Amazon Cognito can provide user sign-up, sign-in, and token-based authentication capabilities. API Gateway can be configured to validate supported authorization tokens, depending on the API type and setup.
Authentication identifies the caller. Your application must still enforce authorization: a signed-in user should not automatically have access to every other user's tasks.
AWS Secrets Manager
If the application needs external API credentials or other secrets, Secrets Manager can store and manage them.
DynamoDB access through the Lambda execution role generally does not require a database password. Avoid adding unnecessary credentials simply because other backend architectures use them.
Amazon CloudWatch
CloudWatch can collect Lambda logs, metrics, and alarms. These help you investigate errors, monitor latency, and detect unusual invocation or throttling patterns.
A simple security model
Frontend
|
v
API Gateway
|
+--> Validate authentication
|
v
Lambda Execution Role
|
+--> Only required DynamoDB actions
|
v
DynamoDB Table
The exact configuration depends on the application's requirements, but the core principle stays the same: grant only the permissions required, and verify who is allowed to access each user's data.
8. What Happens When Traffic Increases?
One reason serverless architecture is attractive is that it can reduce the amount of infrastructure management required as demand changes.
However, each component has its own limits and scaling behavior.
API Gateway
API Gateway can handle API traffic and supports configurable throttling and quotas. Your API may still receive throttling responses if configured limits or service quotas are exceeded.
Lambda
Lambda can increase concurrent executions as demand grows, within applicable concurrency quotas and account limits.
But more concurrent Lambda executions can also increase pressure on downstream services. If a function triggers expensive database operations, scaling the function alone will not solve the underlying bottleneck.
DynamoDB
DynamoDB offers on-demand and provisioned capacity modes. The appropriate choice depends on traffic patterns, predictability, and cost requirements.
Your key design and indexes also matter. A poorly designed access pattern can create hot partitions or inefficient queries even when the database itself is managed.
What about cold starts?
A cold start can occur when Lambda needs to initialize a new execution environment before processing a request. This may add latency.
The effect depends on factors such as runtime, initialization work, dependencies, memory configuration, and the execution environment.
For many applications, occasional cold starts are acceptable. For latency-sensitive workloads, measure actual performance and evaluate options such as reducing initialization work or using provisioned concurrency where appropriate.
Serverless makes infrastructure management easier in many cases, but it doesn't remove the need for performance testing.
9. Serverless Is Not Always Cheaper
This is an important lesson: serverless is a deployment model, not a guarantee of lower cost.
With Lambda, costs can depend on request volume, execution duration, configured memory, and other applicable charges.
API Gateway pricing depends on the API type and usage. DynamoDB costs depend on the capacity mode, requests, storage, and additional features.
For a small application with occasional requests, a serverless design can be cost-effective because you don't need to maintain an always-running application server for every request.
For a high-volume application with predictable, sustained workloads, another architecture might be more economical. You need to compare actual usage and current pricing rather than assume one model always wins.
Remember to consider supporting services, logging volume, data transfer, provisioned capacity, and other configuration choices when estimating costs.
10. Lambda vs EC2 vs ECS/Fargate
Since I've been learning EC2, Docker, Amazon ECR, ECS, and Fargate, I find it useful to compare these approaches.
| Feature | AWS Lambda | EC2 | ECS with Fargate |
|---|---|---|---|
| Infrastructure management | AWS manages the execution infrastructure | You manage the instances and operating system | AWS manages the underlying compute infrastructure |
| Application model | Event-driven functions | Applications or services on virtual machines | Containerized applications |
| Scaling | Based on invocations and concurrency settings | Configure scaling for instances and workloads | Configure service/task scaling |
| Long-running processes | Subject to function execution limits | Suitable | Suitable, subject to task and service configuration |
| Runtime control | Supported Lambda runtimes and execution model | High control | Container-level control |
| Operational effort | Often lower for suitable workloads | Often higher | Moderate, depending on the application |
| Good fit | APIs, event handlers, scheduled tasks | Custom server environments and broad workloads | Containerized APIs, workers, and services |
Lambda is especially useful when work can be broken into event-driven executions.
EC2 is useful when you need control over the virtual machine and its environment.
ECS with Fargate is useful when you want to run containers without managing the underlying EC2 instances yourself.
These services are not mutually exclusive. A real application can use Lambda for event-driven operations and ECS/Fargate for a long-running API or worker.
Choose based on the workload, operational requirements, performance expectations, and cost—not just which service sounds newer.
11. What I Would Consider Before Deploying to Production
Before deploying a serverless API for real users, I would check the following:
- [ ] Configure API routes and appropriate authorization.
- [ ] Validate request bodies and query parameters.
- [ ] Derive user identity from verified authentication rather than trusting client-supplied identity fields.
- [ ] Use a least-privilege Lambda execution role.
- [ ] Design DynamoDB keys and indexes around real access patterns.
- [ ] Configure environment variables and manage any required secrets safely.
- [ ] Add useful logs, metrics, alarms, and tracing where appropriate.
- [ ] Handle errors and retries carefully.
- [ ] Make operations safe to retry where duplicate processing is possible.
- [ ] Set suitable timeouts, concurrency limits, and throttling.
- [ ] Test under realistic traffic and estimate costs.
- [ ] Deploy through a repeatable infrastructure-as-code and CI/CD process.
One detail worth emphasizing is idempotency. Distributed systems can retry requests or events after failures. If a repeated operation would create duplicate records, charge a customer twice, or trigger the same action repeatedly, the application needs a strategy to prevent that.
For asynchronous workflows, services such as Amazon SQS can help decouple components. But queue-based processing also requires careful retry handling, dead-letter queues where appropriate, and idempotent consumers.
12. When Should You Use This Architecture?
A serverless API built with API Gateway, Lambda, and DynamoDB is worth considering when:
- You are building a small or medium-sized API.
- Traffic is variable or difficult to predict.
- You want to reduce server administration.
- Your application fits DynamoDB's data model and access patterns.
- You want to integrate backend operations with events from other AWS services.
- You prefer managed services for the initial deployment.
It may be less suitable when:
- Your application depends heavily on relational queries and complex joins.
- You need sustained, long-running processing beyond Lambda's execution limits.
- You require very predictable low latency and have not accounted for initialization behavior.
- Your workload has steady high utilization and another compute model may offer better economics.
- You need operating-system or runtime control that the chosen serverless service does not provide.
These are trade-offs, not absolute rules. The best architecture depends on the actual requirements.
13. My Main Takeaways
After learning about API Gateway, Lambda, and DynamoDB, I think the biggest benefit of serverless is the change in focus.
Instead of spending as much time managing servers, you can spend more time designing APIs, writing business logic, securing data access, and understanding how application components interact.
Here is the mental model I use:
- API Gateway receives and routes API requests.
- Lambda executes backend logic.
- DynamoDB stores and retrieves data.
- IAM controls AWS permissions.
- Cognito or another identity provider can support authentication.
- CloudWatch helps monitor and troubleshoot the system.
- SQS can decouple asynchronous work when the application needs it.
But serverless does not mean architecture disappears. You still need to design for security, failures, retries, performance, and cost.
That is what makes learning AWS interesting: the goal isn't simply to learn what each service does. It's to understand how to combine the services to solve a real problem.
What's next?
I'm continuing my AWS learning journey by exploring how these services fit into real application architectures, from containerized backends to serverless APIs and automated deployments.
If you're also learning AWS, I'd love to hear what you're building and which services you're exploring.

Top comments (0)