Express Roadmap
Express Roadmap
React client · Next.js full-stack alternative
The ordered build plan for a PERN API — PostgreSQL + Express + Node: nine phases from a blank repo to a monitored production deployment. One phase at a time — no phase starts before the previous deliverable exists. Estimates assume one experienced developer; solo, double them.
This roadmap is the E + N of PERN, the API half. Pair it with the React client roadmap — the two walking skeletons only meet on one shared contract. If you would rather ship frontend and backend as a single deployable, take the Next.js roadmap instead.
The checkboxes are the work. This file is what actually moves the project forward.
Phase 0 — Spec (½–2 days)
Goal: a written definition of the API before any tooling.
- One-paragraph service statement ("This API does [job] for [clients], unlike [alt]")
- Resource inventory: nouns, their CRUD verbs, and auth requirement per endpoint
- Data model draft: entities, IDs, relationships, ownership rules
- Non-functional requirements: auth? realtime? file uploads? webhooks? multi-tenant?
- Consumer decided: the paired React SPA (same deploy cadence, cookie auth) vs a public API (versioning, keys) — this drives the auth style
- Repo shape decided: pnpm monorepo (
apps/api,apps/web,packages/shared) — the default for PERN — vs two repos when the halves ship on fully independent cadences - Contract source of truth picked: shared zod schemas in
packages/shared(monorepo) or an OpenAPI document exported by CI
Deliverable: one page a stranger could build the routes and schema from.
Watch for: write the pagination/filter/sort contract for list endpoints now — retrofitting it in Phase 4 breaks every consumer.
Phase 1 — Foundation (1–2 days)
Goal: an empty but correct service: boots, type-checks, lints, CI green.
- Node 24 LTS installed (22.13+ floor); pinned in
.nvmrc+engines - pnpm workspace initialised;
apps/apiscaffolded, TypeScriptstrict+noUncheckedIndexedAccess - Express 5 (promise-aware error handling; Express 4 is legacy — do not start there)
- Layout:
src/routes/ src/services/ src/db/ src/middleware/ src/lib/ - Config typed and validated at boot: zod schema over
process.env,node --env-file=.env; every key documented in.env.examplewith{{UPPER_SNAKE}}placeholders - ESLint 9 flat config + Prettier + husky + lint-staged
-
GET /healthz(process up) andGET /readyz(DB reachable) answering - CI on every PR: lint → typecheck → tests (GitHub Actions)
Deliverable: pnpm dev serves an empty app that returns 200 on /healthz; CI green.
Watch for: strict flags are cheap now and a migration project in month three — same for the health endpoints your platform will probe.
Phase 2 — Walking skeleton (3–5 days)
Goal: one thin slice of real value, end to end: route → validation → DB → response.
- PostgreSQL 17+ running via docker compose; connection string from env only
- Drizzle ORM + drizzle-kit; first migration for one entity (default: typed, SQL-shaped, no runtime codegen — choose Prisma when you want its heavier migration/admin story)
- One zod schema per endpoint; handlers stay typed: parse request → call service → serialize response
- One feature delivered completely: route → service → repository inside one transaction
- Error envelope: one consistent JSON shape, central error middleware, stack traces never leave the process
- supertest covers the slice against a real Postgres (schema reset per run); CI green
Deliverable: a demoable API with one genuinely useful resource, covered by tests.
Watch for: Express 5 forwards rejected promises from async handlers to your error middleware — on Express 4 you must wrap every handler yourself.
Phase 3 — Core capabilities (1–2 weeks)
Goal: the machinery every later endpoint needs exists once, correctly.
- Auth flow: register/login, argon2id hashing, JWT access (15 min) via
jose+ rotating refresh token in anhttpOnly,SameSitecookie -
requireAuth/requireRole/ ownership helpers; every route declares its requirement explicitly - CORS allowlist = the web origin(s) with
credentials: true; helmet security headers - Request-ID middleware + structured logging (pino) with the ID propagated
- Rate limiting on auth and public endpoints (express-rate-limit), sane per-IP defaults
- Pagination/filter/sort convention applied to every list route
- Background jobs only where a job outlives the request: BullMQ + Redis, enqueued from the handler
- Shared zod schemas exported from
packages/sharedso the client imports instead of re-declaring
Deliverable: new routes become mostly composition — auth, validation, logging included.
Watch for: refresh-token rotation must invalidate the previous token on use — otherwise you do not have rotation, you have a second lifetime.
Phase 4 — Feature build-out (weeks, variable)
Goal: the remaining product features, each as a vertical slice.
- Feature per slice, same internal layout: route → schemas → service → repository
- File uploads streamed to object storage (S3/R2) — never written to the app container's disk
- Third-party integrations behind typed clients; webhook endpoints verify signatures and are replay-protected
- Realtime where the product needs it: SSE first (one-way), WebSockets only when bidirectional
- OpenAPI generated from the zod schemas; reviewed as a consumer would read it
- Idempotency keys on money-ish POSTs
Deliverable: feature-complete against the Phase 0 spec.
Phase 5 — Quality (3–5 days + ongoing)
Goal: confidence that change does not break things.
- Unit tests on services and domain logic (Vitest); validators tested as pure functions
- Endpoint tests (supertest) on a real database — mocking your own ORM proves nothing
- Contract tests: OpenAPI snapshot diffed in CI
- Performance budget set and measured: p95 latency, RPS ceiling (autocannon), numbers recorded
- N+1 audit on every list route; pool
maxsized against Postgresmax_connections× replicas - Timeouts and bounded retries on every outbound call
Deliverable: green suite in CI + recorded performance numbers.
Phase 6 — Hardening (2–4 days)
Goal: safe to hand to strangers.
- Security pass against the OWASP ASVS shortlist: injection (parameterized/ORM only), mass assignment (explicit serializers — never
res.json(req.body)), authZ matrix, rate limits - Secrets out of the repo;
.env.exampledocuments every setting - Logs scrubbed of tokens/PII; Sentry receiving errors with release tags
- Dependency vulnerability scan in CI (
pnpm audit/ osv-scanner); Renovate on - Cookies reviewed:
httpOnly,Secure,SameSite, correctPath
Deliverable: a passed checklist and a live error dashboard.
Phase 7 — Release (2–5 days)
Goal: serving production traffic behind HTTPS.
- Production Dockerfile: multi-stage, non-root, pinned Node LTS base, dist-only runtime
- Host decided: container platform (Fly/Render/Railway/K8s) vs batteries-included PaaS; TLS at the platform or Caddy in front
- Migrations run as a release step, expand-contract by policy (one release of backward compatibility)
- Graceful shutdown rehearsed: SIGTERM → stop accepting → drain in-flight →
pool.end() - Staging with production-shaped config; smoke test runs against it before promote
- Rollback rehearsed: previous image + migrate-forward policy
Deliverable: 1.0 serving real traffic; rollback tested.
Phase 8 — Operate & iterate (forever, ~1 h/week)
Goal: the service gets better instead of older.
- Weekly triage: error rates, p95 latency, slow queries (
pg_stat_statements), pool saturation - Dependency and Node LTS upgrades on a cadence (Renovate weekly; majors in a branch)
- Quarterly: re-run the security checklist, revisit ADRs, rotate credentials
- Monthly cost glance: DB size, transfer, idle replicas
Rules that keep this roadmap honest
- Walking skeleton before features. One thin end-to-end slice beats five half-built routers.
- No phase skips. Tests after Phase 5 are cheap; tests retrofitted in Phase 7 are not.
- Deliverables are binary. Each phase ends with something that exists or does not.
- Cut scope, not structure. A smaller feature set in a sound codebase ships; the reverse rarely does.
- Re-estimate at each phase boundary. The roadmap is a living document — update it as reality diverges.