/library / template-node-express-api
templateAPI / Backend

Node Express REST API with Drizzle + Zod

A typed Node Express API. Drizzle ORM for Postgres, Zod schemas at the route boundary, JWT auth, Pino logging, Vitest for tests. Deploy anywhere a long-running Node process runs.

use whenBackend for a mobile app or SPA. You want a long-running Node process and a real ORM, not edge functions.

May 8, 20262,360 bytesnodeexpressdrizzlezodpostgres

[API Name]

A REST API. Single Node process. Postgres in the back.

Source of truth

Production runs on Fly.io (one VM, one Postgres). Local dev mirrors prod via fly proxy 5432 -a yourapp-db. The deployed image is the source of truth for what handles requests.

Tech stack

Node 22 + Express 5 + TypeScript. Drizzle ORM (typed SQL queries, generated migrations). Zod for request validation. JWT auth with refresh. Pino for structured JSON logging. Vitest for unit tests. Supertest for route tests. Deployed via fly deploy.

Deploy

fly deploy from local. Postgres lives in the same Fly region. Secrets via fly secrets set. Logs via fly logs.

File map

  • src/index.ts app entrypoint, port + signal handling
  • src/routes/ route modules, one per resource
  • src/middleware/ auth, rate limit, error handler
  • src/db/schema.ts Drizzle table definitions
  • src/db/migrations/ generated SQL migrations
  • src/lib/auth.ts JWT sign + verify
  • src/lib/logger.ts Pino setup
  • tests/ Vitest + Supertest specs
  • drizzle.config.ts schema + out dir
  • fly.toml Fly app config

.env keys

  • DATABASE_URL
  • JWT_SECRET
  • JWT_REFRESH_SECRET
  • PORT defaults 8080
  • NODE_ENV development | production
  • LOG_LEVEL info | debug | error

Hard rules

  • Every route validates request body with Zod before any logic.
  • DB queries via Drizzle, no raw SQL strings in handlers (except in db/).
  • All handlers wrapped with asyncHandler so errors hit the central error middleware.
  • Logs are JSON. No console.log in production code paths.
  • Migrations generated via drizzle-kit generate, applied via drizzle-kit migrate. Never edit migrations by hand.
  • Rate limit on /api/auth/* routes. 5 attempts per minute per IP.

Recent significant changes

  • 2026-05-08: Scaffolded. Locked: Drizzle over Prisma (Edge-friendlier, smaller binary). Express 5 over Fastify (boring + familiar). Fly over Vercel (long-running process).

Next session: start here

  1. fly launch to create the app + Postgres.
  2. fly secrets set for JWT_SECRET, JWT_REFRESH_SECRET.
  3. npm run db:generate then npm run db:migrate.
  4. Implement first resource route + Zod schema + test.
  5. Smoke-test JWT flow with a real client before connecting frontend.

Get the next CLAUDE.md in your inbox.

One new template every week, plus occasional case studies.