Building an API on AWS is one thing. Understanding what happens between the client request, API Gateway, Lambda, DynamoDB, IAM, and the deployment pipeline is another.
For this hands-on project, I built a complete serverless CRUD API using Amazon API Gateway, AWS Lambda, and Amazon DynamoDB.
The goal was to understand how these services work together and to build the API from the ground up rather than treating the architecture as a black box.
๐๏ธ What I Built
The application follows a simple serverless architecture:
Client
โ
โผ
API Gateway
โ
โผ
AWS Lambda
โ
โผ
DynamoDB
For deployment, GitHub Actions was added to the architecture:
GitHub
โ
โผ
GitHub Actions
โ
โผ
GitHub OIDC
โ
โผ
AWS IAM
โ
โผ
AWS Lambda
The result is a REST-style CRUD API without managing servers.
๐ ๏ธ AWS Services Used
| Service | Purpose |
|---|---|
| Amazon API Gateway | Expose HTTP API endpoints |
| AWS Lambda | Run the application logic |
| Amazon DynamoDB | Store application data |
| AWS IAM | Manage permissions and roles |
| AWS STS | Provide temporary credentials through role assumption |
| GitHub Actions | Automate Lambda deployments |
| GitHub OIDC | Authenticate GitHub Actions with AWS |
๐ API Operations
The API supports the main CRUD operations:
| Operation | HTTP Method | Purpose |
|---|---|---|
| Create | POST | Create a new user |
| Read | GET | Retrieve one or all users |
| Update | PUT | Update an existing user |
| Delete | DELETE | Delete an existing user |
For example:
POST /users
GET /users
GET /users/{name}
PUT /users/{name}
DELETE /users/{name}
The exact API Gateway routes are configured in AWS.
๐๏ธ DynamoDB Design
The project uses a DynamoDB table with:
Partition Key: name
Type: String
A user record looks conceptually like:
{
"name": "Ali",
"email": "ali@example.com",
"followers": 10
}
One important DynamoDB concept I worked through was the role of the primary key.
Because name is the partition key, it uniquely identifies the item in this table.
๐ Get One vs Get All
One of the concepts I wanted to understand properly was the difference between retrieving a specific DynamoDB item and scanning the table.
For a single user, Lambda uses a GetCommand.
For retrieving all users, Lambda uses a ScanCommand.
Conceptually:
GET /users/Ali
โ
โผ
DynamoDB GetItem
โ
โผ
Ali
Whereas:
GET /users
โ
โผ
DynamoDB Scan
โ
โผ
All users
This distinction became particularly useful while testing the API.
โ Create-Only POST Requests
I didn't want a POST request to silently overwrite an existing user.
DynamoDB normally allows a PutItem operation to replace an existing item with the same primary key.
To make the endpoint behave like a create operation, I added a condition:
ConditionExpression: "attribute_not_exists(#name)"
Because name is used as the partition key, this condition prevents an existing record from being replaced.
A duplicate request therefore returns:
HTTP 409 Conflict
instead of overwriting the existing record.
โ๏ธ Updating Records
The PUT endpoint uses DynamoDB's UpdateCommand.
It updates the user's fields and returns the updated item.
For example:
PUT /users/Ali
can update values such as:
{
"email": "new-email@example.com",
"followers": 25
}
The updated record is then returned by the API.
๐๏ธ Deleting Records
The DELETE endpoint first checks whether the user exists.
If the user exists, Lambda removes the DynamoDB item.
If the user does not exist, the API returns:
HTTP 404 Not Found
This keeps the API behavior predictable for the client.
โก Lambda
The API logic runs inside AWS Lambda.
The function handles:
- Reading the API Gateway request
- Identifying the HTTP method
- Reading path parameters
- Parsing request bodies
- Validating input
- Performing DynamoDB operations
- Returning HTTP responses
The Lambda function uses the AWS SDK for JavaScript.
The project uses the modular AWS SDK v3 packages:
@aws-sdk/client-dynamodb
@aws-sdk/lib-dynamodb
๐ API Gateway
Amazon API Gateway provides the HTTP interface for the Lambda function.
The API exposes the CRUD routes and forwards requests to Lambda.
The overall request flow is:
HTTP Request
โ
โผ
API Gateway
โ
โผ
Lambda
โ
โผ
DynamoDB
โ
โผ
Lambda Response
โ
โผ
API Gateway
โ
โผ
Client
I tested the endpoints externally through API Gateway and Postman.
๐ IAM Roles
There are two different IAM responsibilities in this project.
Lambda Execution Role
The Lambda execution role allows the function to interact with DynamoDB.
Conceptually:
Lambda
โ
โผ
Lambda Execution Role
โ
โผ
DynamoDB Permissions
GitHub Deployment Role
GitHub Actions uses a separate IAM role to deploy the Lambda function.
GitHub Actions
โ
โผ
GitHub OIDC
โ
โผ
Deployment IAM Role
โ
โผ
Lambda Deployment
Keeping these roles separate helped reinforce an important AWS principle:
The identity deploying a function does not need to be the same identity used by the function at runtime.
๐ GitHub OIDC Deployment
The Lambda deployment was automated using GitHub Actions.
Instead of storing long-lived AWS access keys inside GitHub, the workflow uses GitHub OIDC.
The flow looks like:
GitHub Actions
โ
โผ
OIDC Token
โ
โผ
AWS IAM
โ
โผ
Temporary Credentials
โ
โผ
Lambda Deployment
This was one of the main reasons I wanted to include CI/CD in the project.
โ๏ธ Deployment Workflow
The workflow performs the following steps:
Checkout repository
โ
โผ
Install dependencies
โ
โผ
Prepare Lambda package
โ
โผ
Authenticate with AWS
โ
โผ
Deploy Lambda
The Lambda package contains the application code and the required dependencies.
I also separated the deployment package from the rest of the repository so files such as tests, documentation, and GitHub configuration are not unnecessarily uploaded to Lambda.
๐งช Testing
The API was tested both through API Gateway and locally.
The final CRUD flow was tested as:
GET all users
โ
GET specific user
โ
POST new user
โ
GET new user
โ
PUT user
โ
GET updated user
โ
DELETE user
โ
GET deleted user
โ
404 response
I also tested the duplicate-create scenario.
Attempting to create an existing user returns:
409 Conflict
which confirms that the DynamoDB condition is working as intended.
๐งฏ Some Problems I Had to Solve
The most useful part of the project was probably the troubleshooting.
Lambda Deployment Permissions
The GitHub deployment role initially did not have all the Lambda permissions required by the deployment action.
The missing permissions were identified from the deployment error and the IAM policy was updated accordingly.
Lambda Package Contents
The first deployment approach packaged the entire repository.
That meant files such as:
README.md
test.js
.github/
could end up inside the Lambda deployment package.
I changed the workflow to create a dedicated lambda-package directory containing only the Lambda code, package metadata, and required dependencies.
ES Modules
The Lambda code uses ES module syntax.
Running the same code locally initially produced an error because Node.js was treating the project as CommonJS.
Adding the appropriate module configuration to package.json allowed the same code to run locally.
Duplicate Data
During testing, attempting to create the same user again correctly returned a 409.
This was a useful reminder that test data can affect subsequent runs and that negative test cases are just as important as successful ones.
๐ Security Considerations
The project was designed without putting AWS credentials into the GitHub repository.
The main authentication approach is:
- GitHub OIDC
- IAM roles
- Temporary AWS credentials
- Separate Lambda execution and deployment roles
- Specific AWS permissions for each role
No access keys, passwords, tokens, or other credentials are included in the repository.
๐ Project Structure
aws-serverless-crud-api/
โ
โโโ .github/
โ โโโ workflows/
โ โโโ deploy.yml
โ
โโโ index.js
โโโ package.json
โโโ package-lock.json
โโโ test.js
โโโ README.md
๐ก What I Learned
This project gave me hands-on experience with:
AWS Lambda
- Lambda functions
- Event handling
- Execution roles
- Error handling
- Lambda deployment
API Gateway
- HTTP APIs
- Routes
- Path parameters
- HTTP status codes
- Connecting API Gateway to Lambda
DynamoDB
- Tables
- Partition keys
- GetItem
- Scan
- PutItem
- UpdateItem
- DeleteItem
- Conditional expressions
IAM
- Trust policies
- Permission policies
- Execution roles
- Deployment roles
- Least-privilege access
GitHub Actions
- CI/CD workflows
- OIDC authentication
- AWS role assumption
- Lambda deployment
๐ฏ Why I Built This
I wanted to go beyond simply learning what Lambda, API Gateway, and DynamoDB are.
The goal was to build something where I had to deal with the actual interactions between these services.
The project also gave me the opportunity to work through IAM permissions, deployment packaging, API error handling, DynamoDB conditions, and CI/CD.
That made the architecture much easier to understand because every part of it was actually configured and tested.
๐ฎ Possible Improvements
There are several directions this project could be taken next:
- Add API authentication and authorization
- Add request validation
- Introduce Amazon Cognito
- Add CloudWatch dashboards and alarms
- Add infrastructure as code
- Add automated API tests to the CI/CD pipeline
- Add a frontend application
- Introduce API versioning
These would move the project closer to a production-style serverless application.
๐ Final Architecture
โโโโโโโโโโโโโโโโโโโโ
โ Client โ
โ Browser / Postmanโ
โโโโโโโโโโฌโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโ
โ API Gateway โ
โ HTTP API โ
โโโโโโโโโโฌโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโ
โ AWS Lambda โ
โ CRUD Logic โ
โโโโโโโโโโฌโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโ
โ DynamoDB โ
โ User Records โ
โโโโโโโโโโโโโโโโโโโโ
Deployment:
GitHub
โ
โผ
GitHub Actions
โ
โผ
GitHub OIDC
โ
โผ
AWS IAM
โ
โผ
AWS Lambda
๐จโ๐ป Project
Built as part of my hands-on AWS learning journey, with a focus on understanding serverless architecture, REST APIs, DynamoDB operations, IAM, and automated AWS deployments.
Top comments (0)