All Node.js templates

Node.js + Express + TypeScript

Express in TypeScript: middleware, error handling, validation, structure.

DevZone Tools2,960 copiesUpdated Apr 6, 2026Node.jsTypeScript
# 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 Node.js templates