A serverless task manager built on API Gateway, Lambda, and DynamoDB, with a static HTML/CSS/JS frontend hosted on S3. Tasks can be added, edited, marked done, filtered, and deleted, and every change is persisted in DynamoDB.
Browser (index.html/style.css/script.js)
│ fetch()
▼
Amazon API Gateway (REST API) ── GET / POST / PUT / DELETE / OPTIONS
│
▼
AWS Lambda (lambda_function.py)
│
▼
Amazon DynamoDB (single table, partition key: taskId)
The frontend is a static site — it can be hosted from S3, GitHub Pages, or opened locally — and only ever talks to the API over HTTPS. There is no server-side rendering and no build step.
| Path | Purpose |
|---|---|
| index.html | App markup |
| style.css | Styling, incl. dark mode & responsive layout |
| config.js | The one line you edit per environment: API_BASE_URL |
| script.js | All frontend logic (fetch calls, rendering, state) |
| lambda_function.py | API backend (CRUD against DynamoDB) |
| tests/test_lambda_function.py | Unit tests for the backend (uses moto to mock DynamoDB) |
| template.yaml | AWS SAM template that provisions the whole stack |
- Add, edit, complete/uncomplete, and delete tasks
- Filter by All / Active / Done, with a "clear completed" action
- Optimistic UI updates with rollback on failure
- Loading, empty, and error (with retry) states
- Keyboard-friendly (Enter to add/save, Escape to cancel an edit) and screen-reader-friendly (labelled controls, live region for status messages)
- Light/dark theme follows the OS preference
- Task text is always rendered via
textContent, neverinnerHTML, so task content can never execute as HTML/script (stored-XSS safe)
TABLE_NAMEandALLOWED_ORIGINare read from environment variables (seetemplate.yaml) rather than hardcoded, so the same code deploys to any environment.- Every write is validated (non-empty task text, length limit, allowed
status values) and returns
400on bad input instead of a raw stack trace. - Updates and deletes use a DynamoDB
ConditionExpressionso a request against a missingtaskIdreturns404instead of silently succeeding.PUTaccepts either or both oftaskandstatus, so editing a task's text is a first-class operation, not just a status flip. - Unexpected errors are logged server-side and return a generic
500/502message — internal exception details are never sent to the client. GETpaginates throughScan(LastEvaluatedKey) instead of assuming the whole table fits in one page, and results are returned oldest-first.
The API has no authentication — anyone with the URL can read, add, edit, or delete tasks. That's acceptable for a personal demo/portfolio project behind an unguessable URL, but before using this for anything real you should add one of:
- An API Gateway API key + usage plan (quick, coarse-grained)
- Amazon Cognito or a Lambda authorizer (per-user auth, needed if multiple people will use the app and shouldn't see each other's tasks)
- At minimum, set
ALLOWED_ORIGIN(see below) to your actual site origin instead of*, and consider AWS WAF for basic rate limiting/abuse protection.
Requires the AWS SAM CLI and AWS credentials configured locally.
sam build
sam deploy --guidedsam deploy --guided walks you through stack name, region, and the
AllowedOrigin parameter, and remembers your answers in samconfig.toml
(gitignored) for future deploys. When it finishes, note the ApiUrl and
WebsiteUrl outputs:
aws cloudformation describe-stacks --stack-name <your-stack-name> \
--query "Stacks[0].Outputs"Put the ApiUrl value into config.js:
const APP_CONFIG = {
API_BASE_URL: "https://<api-id>.execute-api.<region>.amazonaws.com/prod",
};Then upload the frontend to the bucket the stack created:
aws s3 sync . s3://<website-bucket-name> \
--exclude "*" --include "index.html" --include "style.css" \
--include "config.js" --include "script.js" --include "Web-Logo.png"Open the WebsiteUrl output in a browser.
- DynamoDB – create a table (any name) with partition key
taskId(String). - Lambda – create a Python 3.12 function, paste in
lambda_function.py, and set environment variablesTABLE_NAME(your table's name) andALLOWED_ORIGIN(your site's origin, or*for local testing). Attach a role withdynamodb:GetItem/PutItem/UpdateItem/DeleteItem/Scanon that table. - API Gateway – create a REST API with a single resource (
/) that proxiesGET,POST,PUT,DELETE, andOPTIONSto the Lambda function, then deploy a stage (e.g.prod). - S3 – create a bucket, enable static website hosting, and upload
index.html,style.css,config.js,script.js, andWeb-Logo.png. - Edit
config.jswith the API Gateway invoke URL from step 3 and re-upload it.
The frontend needs no build step or backend running locally — it just needs
to be served over http:///https:// (not file://, since some browsers
restrict fetch from local files) and pointed at a deployed API:
python -m http.server 8000
# open http://localhost:8000python -m venv .venv
source .venv/bin/activate # or .venv\Scripts\activate on Windows
pip install -r requirements-dev.txt
pytestTests mock DynamoDB with moto, so no
AWS account or credentials are needed to run them.
Base URL is whatever config.js / API Gateway gives you.
| Method | Body | Response | Notes |
|---|---|---|---|
GET |
— | 200 array of tasks |
Sorted oldest-first |
POST |
{"task": "..."} |
201 created task |
400 if task is missing/empty/too long |
PUT |
{"taskId": "...", "task"?: "...", "status"?: "pending"|"done"} |
200 updated task |
400 invalid input, 404 unknown taskId |
DELETE |
{"taskId": "..."} |
200 {"deleted": taskId} |
404 unknown taskId |
- Add authentication (Cognito or an API key) so this is safe to expose publicly
- Move the frontend behind CloudFront + HTTPS instead of the plain S3 website endpoint
- Add due dates / priorities and a corresponding DynamoDB GSI for sorting
- Add end-to-end tests (e.g. Playwright) against a deployed stack in CI