A production-grade Node.js + Express REST API boilerplate built on TypeScript 7, PostgreSQL, and Prisma — a complete starter template with JWT authentication and refresh-token rotation, Zod-driven OpenAPI documentation, and Docker-based dev/stage/prod environments. It's built around a small reference domain — auth, inventory, orders, and reports — to demonstrate real patterns (layered architecture, transactional writes, environment-parity containers) rather than a bare "hello world" starter.
It runs on TypeScript 7, the native Go-ported TypeScript compiler — see Why TypeScript 7? for what that buys the project.
- Features
- Why TypeScript 7?
- Tech Stack
- Prerequisites
- Getting Started
- Environment Configuration
- Available npm Scripts
- Docker & Make Targets
- Project Architecture
- Testing Strategy
- API Documentation
- Layered, feature-based architecture — routes → controller → service → repository, enforced consistently across every module (see Project Architecture).
- JWT auth with refresh-token rotation — short-lived signed access tokens (
jose), opaque DB-tracked refresh tokens with reuse detection. - Schema-first validation and docs — Zod schemas validate every request and double as the source for an auto-generated OpenAPI spec, served live at
/docs. - Transactional writes where it matters — e.g. placing an order atomically decrements stock and creates the order in one database transaction.
- Environment-parity Docker setup — one
Dockerfile, one basedocker-compose.yml, and a thin overlay per environment (dev/stage/prod), all driven through a singleMakefile. - Layered test strategy — fast fake-repository unit tests with no database, plus real-Postgres integration tests against a fully isolated, disposable test stack.
This project intentionally runs on TypeScript 7 (typescript@^7.0.2, released 2026-07-08) instead of TypeScript 6 — a from-scratch, Go-ported reimplementation of the compiler and language service, not a new language version. The type system and tsconfig.json options you already know are unchanged; what changes is the engine running them.
The practical payoff is speed: native compiled code lets tsc type-check and build a codebase like this one substantially faster than the JavaScript-based compiler did, which is felt directly in this project's npm run typecheck step, npm run build, and every CI run that shells out to either.
The trade-off is ecosystem maturity, not capability:
typescript-eslintdoesn't support TS7 yet — it hard-throws at import time (tracked upstream at typescript-eslint#10940). This project uses Biome for linting and formatting instead of ESLint as a result.npm run typecheck(nativetsc --noEmit) remains the actual source of truth for type errors either way — Biome only lints style and common mistakes, it does not type-check.tsx(the dev-time runner) is esbuild-based and never touches thetypescriptpackage's compiler API, so it's unaffected and behaves exactly as it would under TS6.
If typescript-eslint ships TS7 support later, swapping back to ESLint is a contained, optional follow-up — nothing here depends on Biome specifically.
| Layer | Choice |
|---|---|
| Runtime | Node 22, Express 5 |
| Language | TypeScript 7 (tsc for build/typecheck, tsx for dev) |
| Database | PostgreSQL via Prisma 7 (driver-adapter based, @prisma/adapter-pg) |
| Auth | JWT access tokens (jose) + rotated, DB-tracked opaque refresh tokens |
| Validation & docs | Zod, doubling as the OpenAPI schema source (@asteasolutions/zod-to-openapi) |
| Testing | Vitest + Supertest |
| Lint / format | Biome |
| Containers | Multi-stage Dockerfile + Compose overlays for dev/stage/prod, via Makefile |
- Node.js ≥ 22.19 and npm
- Docker + Docker Compose (recommended path), or a local PostgreSQL instance if running bare-metal
cp .env.example .env.development # fill in real secrets
make dev-up # builds + starts app (hot-reload) and Postgres together
make migrate # applies migrationsThen:
- API: http://localhost:3000
- Health: http://localhost:3000/health, http://localhost:3000/health/ready
- API docs (Swagger UI): http://localhost:3000/docs
To seed demo data (an admin user, 20 demo users, 40 inventory items, and 150 orders):
make seedStop the stack with make dev-down (keeps data) or make clean (also drops the Postgres volume).
npm install
cp .env.example .env.development # point DATABASE_URL at your own Postgres
npx prisma generate
npx prisma migrate dev
npm run devThree environment files, one per deployment target — only .env.example is committed:
| File | Used by |
|---|---|
.env.development |
npm run dev and make dev-up |
.env.staging |
make stage-up |
.env.production |
make prod-up |
src/config/env.ts validates every variable at boot with Zod and fails fast, with a clear error, if anything required is missing or malformed.
| Command | What it does |
|---|---|
npm run dev |
Start the dev server (tsx, hot-reload, no Docker) |
npm run build |
Compile to dist/ (native tsc) |
npm start |
Run the compiled build |
npm run typecheck |
tsc --noEmit — the real source of truth for type errors |
npm run lint / lint:fix |
Biome lint |
npm run format |
Biome format (writes changes) |
npm run format:check |
Biome format check (no changes, CI-friendly) |
npm test |
Full test suite (needs a reachable Postgres) |
npm run test:unit |
Fake-repository unit tests only — no database needed |
npm run test:integration |
Real-Postgres integration tests |
npm run test:watch |
Vitest in watch mode |
npm run test:coverage |
Full suite with coverage report |
npm run prisma:generate |
Regenerate the Prisma client from schema.prisma |
npm run prisma:migrate |
Create + apply a dev migration |
npm run prisma:migrate:deploy |
Apply already-committed migrations (no new ones generated) |
npm run prisma:seed |
Seed an admin user + sample inventory/orders |
| Target | What it does |
|---|---|
make dev-up / make dev-down |
Build + start app (hot-reload) + Postgres together / stop the dev stack (keeps data) |
make stage-up / make stage-down |
Build + start the immutable runtime image against .env.staging / stop it |
make prod-up / make prod-down |
Build + start the immutable runtime image against .env.production / stop it |
make migrate |
Apply already-committed migrations (prisma migrate deploy); ENV=staging make migrate to target another env |
make makemigrations |
Dev-only: create + apply a new migration from schema changes (NAME=add_foo make makemigrations to skip the naming prompt) |
make seed |
Seed demo data (ENV=staging make seed to target another env) |
make test |
Bring up an isolated test Postgres (own project/port/volume — safe to run even while make dev-up is up), migrate, run the suite on the host, tear back down |
make clean |
Stop the dev stack and drop the Postgres volume |
make down-test |
Recovery: tear down the test stack if a make test run got interrupted |
make logs |
Tail the app container's logs |
Adding another backing service (e.g. Redis) later only means adding one block to docker-compose.yml — every environment overlay and make dev-up/stage-up/prod-up picks it up automatically.
Feature-based modules under src/modules/<feature>/ (auth, health, inventory, orders, reports), each owning its own routes → controller → service(s) → validation → repository. A few rules hold across every module:
- Controllers call only their module's service(s) — never a repository, never Prisma directly.
- Services call only their module's repository (or another module's service, for cross-module composition) — never Prisma directly. This is what makes services unit-testable with fake repositories and no database (see
tests/unit/). - Repositories are the only files that import the Prisma client. Every write method takes an optional trailing
txparameter so it can run standalone or inside someone else's transaction. - Transactions are owned by the service that knows an operation must be atomic — see
orders.service.createOrder(), which wrapsinventoryService.adjustStock()(the module owning the "stock can't go negative" rule) andordersRepository.create()in onewithTransaction()call. - Cross-module reads that don't belong to any single module (the sales-summary report, joining orders + inventory + users) live in
src/shared/repositories/, not inside one module's repository. - Pagination (
src/shared/utils/pagination.ts) is shared across every list endpoint instead of hand-rolled per module.
src/
├── modules/{health,auth,inventory,orders,reports}/
├── shared/{utils,repositories}/ # cross-module concerns
├── middlewares/ # security, validation, error handling
├── lib/ # Prisma client + transaction helper
├── docs/ # OpenAPI registry + generation
└── config/ # env validation, logger
Two distinct suites, kept intentionally separate:
- Unit tests (
tests/unit/) exercise services against fake, in-memory repository implementations (tests/helpers/fakeRepositories/) — no database, no Docker, fast enough to run on every save. - Integration tests (
tests/integration/) exercise the real HTTP layer against a real Postgres database.make testbrings up a fully isolated Postgres (its own Compose project, port, and volume — safe to run even whilemake dev-upis already up), applies migrations, runs the full suite, and tears the stack back down afterward. Each test file resets every table before running (tests/helpers/db.ts), and integration files run sequentially rather than in parallel to avoid one file's reset racing another's in-flight requests.
Always run make test for a full, correct run — invoking npm test directly requires that isolated Postgres to already be up and migrated (see the Makefile's test target for the exact sequence).
Interactive Swagger UI is served at /docs once the app is running, generated directly from the Zod validation schemas via @asteasolutions/zod-to-openapi — request/response shapes in the docs are guaranteed to match what the API actually validates, because they come from the same source.