Building a Zero-Cost Serverless Task Manager on AWS
Some projects are small enough to finish in a few days, but rich enough to teach almost every important cloud concept at once. This one was exactly that for me.
I built a serverless task manager on AWS with three goals in mind:
- keep the architecture fully serverless
- design it with security in mind from the start
- stay as close to zero cost as possible
The result is a web application that supports task CRUD operations while also demonstrating how to combine CloudFront, S3, API Gateway, Lambda, DynamoDB, VPC Endpoints, CloudWatch, SNS, and AWS Budgets into one practical architecture.
What the project does
The app is a simple Task Manager that lets users:
- create a task
- view all tasks
- update task details or status
- delete tasks
The frontend is written in plain HTML, CSS, and JavaScript, while the backend is a Python-based AWS Lambda function connected to DynamoDB.
Even though the business logic is simple, it is a great vehicle for learning the full request flow of a cloud-native application.
Architecture overview
The system is divided into five main layers:
- Frontend layer:
CloudFront + S3 - API layer:
API Gateway + AWS Lambda - Data layer:
DynamoDB + Global Secondary Index - Network layer:
VPC + private subnets + VPC Endpoint - Observability and cost layer:
CloudWatch + SNS + AWS Budgets

At a high level, the request flow looks like this:
- A user opens the website through CloudFront.
- CloudFront fetches static assets from S3 using Origin Access Control.
- The frontend sends API requests to API Gateway.
- API Gateway invokes a Lambda function for
GET,POST,PUT, andDELETEoperations. - Lambda reads and writes task data in DynamoDB.
- Logs and metrics are collected in CloudWatch, while SNS and AWS Budgets handle alerts.
This architecture stays compact, but still reflects real design concerns around security, scaling, and operational visibility.
Why I chose a serverless architecture
I picked serverless for three practical reasons.
1. The workload is event-driven
A task manager does not need a server running all day. Requests come in occasionally, and each request is short-lived. Lambda is a natural fit because compute only runs when needed.
2. It reduces operational overhead
With EC2 or self-managed containers, I would need to worry about patching, provisioning, scaling, and instance-level monitoring. With managed serverless services, AWS takes care of most of that undifferentiated heavy lifting.
3. It makes cost control easier
For learning projects and portfolio work, cost matters. Managed services with pay-per-use pricing are much easier to keep under control than always-on infrastructure.
How I built it
This is the step-by-step approach I followed while building the project.
Step 1. Host the frontend privately with S3 and CloudFront
I used an S3 bucket to store the static frontend files and placed CloudFront in front of it to distribute content securely.
The key decision here was not to expose the bucket publicly. Instead, I configured Origin Access Control (OAC) so only CloudFront can fetch objects from S3.
That gave me three benefits:
- the bucket can keep Block Public Access enabled
- direct access to S3 objects is denied
- users can still access the website normally through CloudFront



This was one of the first moments where the project shifted from "just make it work" to "make it work properly."
Step 2. Expose CRUD operations with API Gateway and Lambda
The backend is a single Python Lambda function that handles:
GET /tasksPOST /tasksPUT /tasks/{id}DELETE /tasks/{id}OPTIONSfor CORS preflight
The function:
- generates
taskIdwithuuid - stores timestamps in ISO format
- dynamically builds update expressions for editable task fields
- returns CORS headers so the frontend can call the API safely from the CloudFront domain
This setup is small, but it already demonstrates a real API pattern: static frontend, stateless compute, and managed persistence.
Step 3. Store task data in DynamoDB
I used DynamoDB as the primary data store. Each task includes fields such as:
taskIduserIdtitledescriptionprioritydueDatestatuscreatedAt
The main partition key is taskId, but I also needed to query all tasks belonging to a user. To support that access pattern, I added a Global Secondary Index on userId.
This was a good reminder that DynamoDB design is driven less by "what columns do I have?" and more by "how will I read this data?"
Step 4. Keep Lambda inside a VPC without using a NAT Gateway
This was probably the most important design choice in the entire project.
When Lambda is attached to a VPC and still needs to access DynamoDB, a common instinct is to add a NAT Gateway. But NAT Gateway has a fixed cost that can easily dominate a small student project.
So instead of using NAT, I:
- attached Lambda to the VPC
- configured the subnets and route tables
- created a VPC Endpoint for DynamoDB
That allowed Lambda to communicate with DynamoDB through private AWS networking, without needing internet egress through NAT.


This one decision made a huge difference. It improved both security and cost efficiency at the same time.
Step 5. Apply IAM least privilege
Instead of using one overly permissive role, I separated responsibilities with dedicated IAM roles and attached more specific policies where possible.
That means:
- Lambda only gets permissions related to the resources it actually uses
- access policies are scoped to concrete ARNs
- the architecture avoids the "just give it admin access so it works" trap

This may look like a small detail in a school project, but I think it is one of the best habits to build early.
Step 6. Add monitoring, alarms, and budget alerts
Once the app was working, I wanted to answer a few operational questions:
- Are requests succeeding?
- Is the Lambda function failing?
- Can I see usage trends over time?
- Will I notice quickly if the cost starts going up?
To answer those, I added:
- a CloudWatch Dashboard
- CloudWatch Alarms
- SNS email notifications
- an AWS Budget



This turned the project from a working demo into something much closer to a real production mindset.
A quick look at the frontend and backend
The frontend is intentionally simple. It focuses on usability rather than framework complexity:
- form inputs for title, description, priority, status, and due date
- task list rendering on page load
- edit and delete actions for each task
- basic client-side validation
- HTML escaping to reduce XSS risk in rendered content
The backend keeps the logic equally straightforward:
- receive API Gateway events
- route based on HTTP method and path
- read and write data in DynamoDB
- return structured JSON responses
- expose CORS headers for browser-based access
This simplicity was intentional. I wanted the cloud architecture to be the star of the project.
What keeps the cost close to zero
The most useful lesson from this project is that cost optimization is often about architecture decisions, not micro-optimizations.
These choices mattered the most:
- using Lambda instead of an always-on server
- using DynamoDB on-demand billing
- hosting static files on S3
- serving the site through CloudFront
- replacing NAT Gateway with a VPC Endpoint
- creating an AWS Budget to catch surprises early
The VPC Endpoint decision is the clearest example. One networking choice can save far more money than dozens of small code-level optimizations.
What I learned from this project
This project taught me much more than how to connect AWS services together.
Security works best when designed in from the beginning
Private S3 access, OAC, least-privilege IAM, and private DynamoDB connectivity all became easier because they were part of the architecture from the start instead of afterthoughts.
Observability is not optional
Dashboards, logs, alarms, and budget alerts are not "bonus features." They are how you understand what your system is doing after deployment.
Serverless is powerful when the workload fits
For low-to-medium traffic, event-driven workloads with simple compute requirements, serverless can provide a strong mix of scalability, low maintenance, and cost efficiency.
DynamoDB forces you to think in access patterns
The GSI design reinforced an important NoSQL lesson: the way you query data should drive how you model it.
If I continue this project, I would improve
If I turn this into a more production-ready version, the next upgrades I want are:
- user authentication with Cognito or JWT-based auth
- splitting one Lambda into multiple smaller functions
- Infrastructure as Code with Terraform or AWS SAM
- CI/CD for deployment
- a custom domain for the frontend and API
- stronger integration testing
Final thoughts
What I like most about this project is not that it uses many AWS services. It is that each service supports a clear goal:
- S3 + CloudFront for secure frontend delivery
- API Gateway + Lambda for stateless backend logic
- DynamoDB for scalable storage
- VPC Endpoint for private, low-cost connectivity
- CloudWatch + SNS + Budgets for visibility and control
In other words, the project is not just "serverless because AWS has cool services." It is serverless because that architecture fits the problem well.
If you are learning cloud and want a portfolio project that feels practical without becoming overwhelming, a system like this is a great place to start.
Project evidence
Here are a few more screenshots from the implementation and validation process.

If you want, I can also turn this into:
- a shorter LinkedIn post
- a portfolio project summary
- a resume-ready project description