Zstem

Serverless API

by Zstem · published 2026-07-16 · 0 forks

APIDatabaseArchitectureDocsSequenceserverlessauth

Serverless API

Open & fork this on Zstem →
ClientCognito User PoolAPI Gateway (REST)Tasks HandlerTasks TableCloudWatchServerless APIzstem.design

Architecture

Sequence

Participants: Client, API Gateway, Cognito, Tasks Handler, Tasks Table

API contract

Database

tasks
pkPartition key: USER#<userId> — userId comes from the Cognito JWT sub claim
skSort key: TASK#<taskId> (taskId is a UUIDv4 minted on create)
task_id
user_idCognito sub of the owner
title
description
statusopen | in_progress | done
due_dateISO-8601; optional
gsi1pkGSI1 partition: USER#<userId>#STATUS#<status> — powers list-by-status
gsi1skGSI1 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.

Open & fork this on Zstem →