Node.js + Express + TypeScript
Express in TypeScript: middleware, error handling, validation, structure.
# CLAUDE.md — Node.js + Express + TypeScript
## Setup
- Node 20+ (LTS). Express 4 (LTS) — Express 5 if you can adopt the breaking changes.
- ESM only. `"type": "module"`. Use `tsx` in dev, `tsc` for production builds.
- Lock the major version of every prod dependency. `npm audit` in CI.
## Project layout
```
src/
app.ts # buildApp(): Express
server.ts # entry point — reads env, starts listening
routes/
users.ts
controllers/
services/
repositories/
middleware/
lib/
errors.ts # custom error classes
test/
```
- `app.ts` builds and returns the Express instance. `server.ts` is the only file that calls `.listen()`.
- One router per resource. Routers compose in `app.ts`.
## Middleware
- Order matters: parse body → log → auth → routes → error handler.
- Use **helmet** for default security headers.
- Use **cors** with an allowlist — never `origin: "*"` in production.
- Body parser via `express.json({ limit: "1mb" })`. Reject huge bodies at the edge.
## Async handlers
- All handlers are `async`. Use `express-async-errors` (or wrap in a `asyncHandler` helper) so thrown errors hit the error middleware.
- Don't write `try/catch` in every handler — push errors to the centralized error handler.
## Error handling
- One central error handler at the bottom of the middleware stack:
```ts
app.use((err, req, res, _next) => {
if (err instanceof NotFoundError) return res.status(404).json({ error: err.message })
if (err instanceof ValidationError) return res.status(400).json({ error: err.message, details: err.details })
log.error({ err }, "unhandled")
res.status(500).json({ error: "internal" })
})
```
- Custom errors extend `Error` with a discriminator. Don't string-match error messages.
## Validation
- Validate every request body, query, and param with **Zod** at the controller boundary.
- Don't trust types — TypeScript doesn't validate runtime input. Zod does.
## Logging
- **pino** with `pino-http` for request logs. Bind `req.id` (use `nanoid` or `crypto.randomUUID`) to every log line.
- Don't `console.log` in handlers.
## Configuration
- One `env.ts` with Zod parsing at boot.
- Crash early on missing/invalid env vars — never default-fall-through to a value that "kinda works".
## Don't
- Don't ship without `helmet`, `cors`, and rate limiting.
- Don't put DB calls in controllers. Service → repository → DB.
- Don't write callback-style code. `async/await` everywhere.
- Don't use `req.params`/`req.body` without validating first. The types lie.
Other TypeScript templates
Next.js App Router + TypeScript Rules
Server Components, Server Actions, and TypeScript discipline for the Next.js App Router.
Next.js + Server Actions + Shadcn UI
Forms with Server Actions, Zod validation, and Shadcn UI primitives.
Next.js + Supabase Auth
Auth flows with Supabase SSR, cookies, and Row Level Security policies.
Next.js + Tailwind + Prisma Stack
Full-stack Next.js with Prisma ORM, Tailwind CSS, and Postgres.
Next.js SEO + Metadata Rules
Metadata API, sitemap, robots, OG images, and structured data done right.