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/api scaffolded, TypeScript strict + 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.example with {{UPPER_SNAKE}} placeholders
  • ESLint 9 flat config + Prettier + husky + lint-staged
  • GET /healthz (process up) and GET /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 an httpOnly, SameSite cookie
  • 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/shared so 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 max sized against Postgres max_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.example documents 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, correct Path

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

  1. Walking skeleton before features. One thin end-to-end slice beats five half-built routers.
  2. No phase skips. Tests after Phase 5 are cheap; tests retrofitted in Phase 7 are not.
  3. Deliverables are binary. Each phase ends with something that exists or does not.
  4. Cut scope, not structure. A smaller feature set in a sound codebase ships; the reverse rarely does.
  5. Re-estimate at each phase boundary. The roadmap is a living document — update it as reality diverges.

React client · Next.js full-stack alternative

Built with LogoFlowershow