Serverless API
Serverless API
Open & fork this on Zstem →Architecture
- Client: Web or mobile client. Signs in against Cognito, then calls the API with a Bearer JWT.
- Cognito User Pool: Hosted sign-up/sign-in. Issues ID + access JWTs; API Gateway validates them via a Cognito authorizer on every route.
- API Gateway (REST): Single REST entry point. Cognito authorizer on all routes, request validation from the OpenAPI contract, throttling, and Lambda proxy integration.
- Tasks Handler: Node.js Lambda (proxy integration) implementing all /tasks routes. Reads the caller's userId from the JWT claims — every query is scoped to the caller.
- Tasks Table: Single-table design. PK USER#<userId>, SK TASK#<taskId>; GSI1 supports list-by-status ordered by due date. On-demand capacity.
- CloudWatch: Structured JSON logs, Lambda metrics + p99 latency alarms, and API Gateway access logs.
Sequence
Participants: Client, API Gateway, Cognito, Tasks Handler, Tasks Table
- Client→API GatewayPOST /tasks (Bearer JWT)Create a task. Body validated against the OpenAPI request model before any code runs.
- API Gateway→Cognitovalidate JWT against user-pool JWKS
- Cognito→API Gatewaytoken valid + claims (sub, groups)
- API Gateway→Client401 UnauthorizedMissing, malformed, or expired token — rejected at the gateway; the Lambda never runs.
- API Gateway→Tasks Handlerproxy invoke (event + claims)Handler reads userId from event.requestContext.authorizer.claims.sub.
- Tasks Handler→Tasks TablePutItem USER#<sub> / TASK#<uuid>Condition expression attribute_not_exists(pk) guards against key collisions.
- Tasks Table→Tasks HandlerOK
- Tasks Handler→API Gateway201 + Task JSON
- API Gateway→Client201 Created
API contract
- POST
/tasksCreate a task - GET
/tasksList the caller's tasks - DELETE
/tasks/{taskId}Delete a task - PUT
/tasks/{taskId}Replace a task - GET
/tasks/{taskId}Fetch one task
Database
| pk | Partition key: USER#<userId> — userId comes from the Cognito JWT sub claim |
|---|---|
| sk | Sort key: TASK#<taskId> (taskId is a UUIDv4 minted on create) |
| task_id | |
| user_id | Cognito sub of the owner |
| title | |
| description | |
| status | open | in_progress | done |
| due_date | ISO-8601; optional |
| gsi1pk | GSI1 partition: USER#<userId>#STATUS#<status> — powers list-by-status |
| gsi1sk | GSI1 sort: due_date (ISO-8601), missing-due-date items sort last |
| created_at | |
| updated_at |
About this design
About this design
What this is
The canonical serverless REST API: API Gateway + Lambda + DynamoDB, with Cognito for auth and CloudWatch for observability. The example domain is a per-user task manager (CRUD on /tasks), but the shape is domain-agnostic — swap "task" for any resource your product needs. Everything here scales to zero, has no servers to patch, and costs nothing when idle.
How it works
Auth happens before your code runs. Clients sign in against the Cognito user pool and send a Bearer JWT. A Cognito authorizer on API Gateway validates the token against the pool's JWKS; invalid or expired tokens are rejected with 401 at the gateway — the Lambda is never invoked (see the sequence diagram).
One Lambda, proxy integration. The Tasks Handler implements every /tasks route. It reads the caller's identity from event.requestContext.authorizer.claims.sub and scopes every read and write to that user — there is no way to address another user's data, by construction.
Single-table DynamoDB. Items live under PK = USER#<userId>, SK = TASK#<taskId>. Listing a user's tasks is one Query; fetching one task is a GetItem. GSI1 (USER#<id>#STATUS#<status> → due date) answers "my open tasks by due date" without scans. Writes use condition expressions so updates and deletes only succeed against items the caller owns.
Request validation at the edge. API Gateway validates request bodies against the models in the API contract, so malformed input returns 400 without a cold start.
Observability. The handler logs structured JSON to CloudWatch; API Gateway access logs and Lambda error/latency alarms complete the picture.
How to extend
Add a resource: define its routes in the API contract, add an item type to the table (e.g. SK = PROJECT#<id>), and either extend the handler's router or add a second Lambda per resource — the authorizer covers new routes automatically.
Add background work: enable DynamoDB Streams on the table and attach a consumer Lambda (e.g. send a reminder when due_date approaches), or publish events to EventBridge from the handler.
Add roles: put users in Cognito groups (admin, member); the group claim arrives in the JWT and can gate routes in the handler.
Harden for production: add WAF in front of the gateway, per-route throttling, X-Ray tracing, and a DLQ on any async consumers.
Deploy with CDK, SAM, or Terraform — the design maps one-to-one onto any of them.