Node.js
Node.js rules and best practices for Claude Code.
10.3k
- Node.js
- TypeScript
- Express
- Nestjs
- Prisma
- Postgres
- Docker
6 TEMPLATES
Node.js + Express + TypeScript
# 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.Node.js + Prisma + Postgres
# CLAUDE.md — Node.js + Prisma + Postgres ## Schema - `prisma/schema.prisma` is the source of truth. Every change goes through `prisma migrate dev`. - Datasource: Postgres. `provider = "postgresql"`. - For Vercel/serverless, set both `url` and `directUrl`: ```prisma datasource db { provider = "postgresql" url = env("DATABASE_URL") // pooled (pgbouncer) directUrl = env("DIRECT_URL") // direct (for migrations) } ``` ## Singleton client - One Prisma client per process. Stash on `globalThis` in dev to survive HMR: ```ts const globalForPrisma = globalThis as unknown as { prisma?: PrismaClient } export const db = globalForPrisma.prisma ?? new PrismaClient({ log: ["error"] }) if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = db ``` - `db.$connect()` is lazy; you usually don't need to call it explicitly. ## Migrations - Local: `prisma migrate dev --name <change>`. Reviewed and committed. - CI: `prisma migrate deploy`. Idempotent; safe to run on every deploy. - Never edit a merged migration. Add a new one to fix forward. - For data-only migrations, write a script that runs separately — don't mix DDL and large data updates. ## Queries - `select` to narrow returns. Default returns all scalars — wasteful and leaks fields. - `include` for related records. Pick `select` over `include` when you only need a few fields of the relation. - `findUnique` for primary-key/unique lookups. `findFirst` only when you need ordering or filters. - Cursor pagination over `skip`+`take` for large tables. ## Transactions - Use `db.$transaction` for multi-step writes. Pass an array of promises (sequential) or a callback (interactive): ```ts await db.$transaction(async (tx) => { const order = await tx.order.create({ data }) await tx.audit.create({ data: { orderId: order.id, action: "create" } }) }) ``` - Set `timeout` on long transactions to avoid holding locks. ## Connection pooling - For serverless, **always** front Prisma with pgbouncer (transaction mode) or use Prisma Accelerate. - `connection_limit` in the pooled URL controls per-instance pool size. Multiply by replicas to size the DB. - Direct connections only for migrations and admin scripts. ## Performance - Add an index for every `where` field you use in production. Prisma doesn't infer them — declare with `@@index`. - `EXPLAIN ANALYZE` on slow queries. Prisma generates the SQL; you can paste it into psql. - Batch reads: `findMany` with an `in` clause beats N round-trips. ## Don't - Don't share a Prisma client across short-lived serverless functions without a connection pooler — you'll exhaust the DB. - Don't return Prisma models directly from public APIs. Map to a DTO so internal field changes don't break consumers. - Don't use `db.$queryRawUnsafe`. The safe variants prevent SQL injection. - Don't run migrations from app boot in multi-replica deploys. Race conditions silently corrupt schemas.TypeScript Node.js Backend
# CLAUDE.md — TypeScript Node.js Backend ## Stack defaults - Node 20+ (LTS). Pin major version in `engines`. - Type-stripping: **tsx** for dev, **tsc** for production builds (`outDir: dist`). - ES modules only (`"type": "module"` in `package.json`). No CommonJS in new code. - Package manager: pnpm or npm. Lock file is committed. ## Project layout ``` src/ app.ts # express/fastify/hono app factory server.ts # bootstrap (reads env, starts listening) routes/ services/ repositories/ lib/ types/ test/ dist/ # gitignored tsconfig.json ``` - Separate `app.ts` (factory) from `server.ts` (entry point) so tests can import the app without binding ports. ## tsconfig ```json { "compilerOptions": { "target": "ES2022", "module": "NodeNext", "moduleResolution": "NodeNext", "strict": true, "noUncheckedIndexedAccess": true, "verbatimModuleSyntax": true, "outDir": "dist", "rootDir": "src" }, "include": ["src/**/*"] } ``` - ES module imports use the **full filename** including `.js` (compiled) or rely on `tsx` which handles `.ts` extensions. ## Configuration - One `env.ts` with Zod parsing at boot. Crash early on invalid env. - Read `NODE_ENV` once. Branch in `env.ts`, not throughout the app. - Don't use dotenv at the source — let the orchestrator inject env. `dotenv` only in dev. ## Logging - **pino** — fast, structured, the right answer for production. - Log JSON to stdout. Let the orchestrator collect. - One logger per module: `const log = pino({ name: "users" })`. ## Errors - Custom errors extend `Error` with a `name` and a `code`: ```ts export class NotFoundError extends Error { name = "NotFoundError" constructor(public resource: string, public id: string) { super(`${resource} ${id} not found`) } } ``` - Map domain errors to HTTP statuses at the framework layer. Don't `throw new HttpError(404)` from a service. ## Process lifecycle - Handle `SIGTERM`. Drain in-flight requests, close DB pools, exit clean. - Don't `process.exit(0)` from a request handler. - Uncaught exceptions / unhandled rejections: log + exit. Let the orchestrator restart. ## Testing - **Vitest** or **Node's built-in test runner**. Fast and minimal. - Unit tests for services. Integration tests for routes against a real DB. - Snapshot tests for response shapes — they're cheap insurance. ## Don't - Don't use `require()` in TypeScript. Use `import`. - Don't ship `console.log` in production code. Use the structured logger. - Don't put SQL in route handlers. Repository → service → route. - Don't depend on Node-specific globals (`process`, `Buffer`) in code shared with the browser.Node.js + NestJS
# CLAUDE.md — Node.js + NestJS ## Mental model - NestJS is opinionated DI + decorator-driven HTTP/microservice framework on top of Express or Fastify. - Build for **modules**: a feature is a module that imports its dependencies and exports its public surface. - Don't fight the conventions — when in doubt, do what the docs do. ## Module structure ``` src/users/ users.module.ts users.controller.ts users.service.ts users.repository.ts dto/ create-user.dto.ts update-user.dto.ts entities/ user.entity.ts users.controller.spec.ts users.service.spec.ts ``` - One module per domain. Modules import other modules to use their providers — never reach into another module's `service` directly. - Public surface = whatever the module `exports`. Keep it small. ## Providers (services) - Annotate with `@Injectable()`. Constructor-inject dependencies — never `new` them. - Services hold business logic. Controllers hold parsing and shape; repositories hold data access. - Don't put HTTP concepts (status codes, request objects) in services. ## DTOs & validation - One DTO per request shape. `class-validator` + `class-transformer` decorators on every field. - Enable the global validation pipe in `main.ts`: ```ts app.useGlobalPipes(new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true, transform: true })) ``` - `whitelist: true` strips unknown properties; `forbidNonWhitelisted` rejects them with 400. ## Controllers - Thin. Decorators declare HTTP route, status code, response type. Body delegates to a service. - `@HttpCode(204)` for no-content responses. Don't return `void` and let Nest pick. - Use `@UseGuards(...)` for auth, `@UsePipes(...)` for per-route validation overrides. ## Pipes, guards, interceptors - **Pipes** transform/validate. Use Nest's built-ins (`ValidationPipe`, `ParseUUIDPipe`) before custom ones. - **Guards** authorize. One guard per concern. - **Interceptors** wrap responses (logging, caching, transforms). Don't use them for auth — use guards. ## Configuration - `@nestjs/config` with a Joi or Zod schema. Validate at boot. - Inject `ConfigService` into providers; don't read `process.env` directly. ## Testing - Unit-test services by constructing them with mocked deps. The Nest testing module is for integration tests, not units. - For controller integration, use `Test.createTestingModule(...)` with overrides for external systems. - Use **supertest** for full e2e tests against the bootstrapped app. ## Don't - Don't bypass DI by importing a class and calling `new`. - Don't put DB queries in controllers. - Don't write circular module imports. Refactor — they signal a bad boundary. - Don't expose entities directly in responses. Map to a Response DTO so DB renames don't break clients.Node.js + Fastify
# CLAUDE.md — Node.js + Fastify ## Why Fastify - Schema-first request validation (uses JSON Schema or **TypeBox** / **Zod via plugin**). - Faster than Express on JSON-heavy workloads. - Native plugin system with explicit encapsulation — better than ad-hoc Express middleware ordering. ## Setup - Node 20+. ESM (`"type": "module"`). - TypeScript with `@fastify/type-provider-typebox` (or `@fastify/type-provider-zod`) for schema-driven types. - Each plugin is a function that takes the `fastify` instance. ## Project layout ``` src/ app.ts # builds and decorates the fastify instance server.ts # entry point plugins/ # auth, db, swagger, etc. routes/ users.ts # one route file per resource schemas/ # TypeBox / Zod definitions services/ ``` ## Schemas - Every route declares `schema: { body, params, querystring, response }`. Don't ship a route without a response schema — it's a perf and correctness win. - TypeBox example: ```ts const UserParams = Type.Object({ id: Type.String({ format: "uuid" }) }) fastify.get("/users/:id", { schema: { params: UserParams } }, async (req) => { ... }) ``` - Validation runs at the boundary; the handler gets typed inputs. ## Plugins - One plugin per concern. `db.ts`, `auth.ts`, `metrics.ts`. - Use `fastify-plugin` for plugins that decorate the root instance — encapsulation breaks otherwise. - Lifecycle hooks (`onRequest`, `preHandler`, `onResponse`) are local to a plugin/encapsulated scope. Use that on purpose. ## Errors - `app.setErrorHandler(...)` once. Map known errors to status codes. - Throw `app.httpErrors.notFound("user")` etc. — Fastify ships an HTTP errors helper. - Validation errors are 400 by default; customize the message format with a `schemaErrorFormatter`. ## Logging - Fastify ships **pino**. Don't add a separate logger. - `request.log` for per-request context. `app.log` for app-level. - Add `app.addHook("onResponse", (req, reply) => req.log.info({ status: reply.statusCode }))` if you want structured access logs. ## Auth - Use **@fastify/jwt** for JWT, **@fastify/oauth2** for OAuth flows. - Auth check via `preHandler` hook on a route or scope. Don't duplicate per route. ## Performance - Set `logger.level: "info"` in production; `"debug"` only when diagnosing. - Enable HTTP/2 only when you have a real reason. It's not a free win. - Use `fastify-compress` only after profiling — gzip CPU isn't free. ## Don't - Don't add Express-style `app.use(middleware)`. Use Fastify hooks. - Don't return raw entity objects without a response schema. Fastify's serializer can't optimize what it can't see. - Don't share state via globals. Decorate the instance: `app.decorate("db", db)`. - Don't call `app.listen` from `app.ts`. That belongs in `server.ts`.Node.js Microservice Patterns
# CLAUDE.md — Node.js Microservice Patterns ## Boundaries - One service per **bounded context**, not per table or per resource. - A service owns its data. No service reads another service's database directly — always through an API. - If two services constantly co-deploy and co-change, they're one service. ## Communication - **Sync** (HTTP/gRPC) for queries that need an immediate answer. - **Async** (events, queues) for state propagation — orders → inventory, user-created → email-welcome. - Default to async. Sync calls between services create cascade failures. ## Idempotency - Every mutation accepts an idempotency key. Same key = same response, even on retry. - Implement with a key→result table indexed on the key. TTL old keys. - Without idempotency, retries (yours or the client's) duplicate work. There will be retries. ## Events - Schema-first. Use **Avro**, **Protobuf**, or at least a **JSON Schema** registry. - Events are immutable facts: `OrderPlaced`, `PaymentSettled`. Past tense. - Emit on commit, not on intent. Outbox pattern: write the event row in the same DB transaction as the state change; a separate process publishes from the outbox. ## Failure handling - Retries are bounded: 3–5 attempts with exponential backoff and jitter. - After max retries, send to a **dead-letter queue**. Alert on DLQ growth. - Circuit breakers (`opossum` or hand-rolled) on every cross-service call. Fail fast when downstream is unhealthy. ## Observability - **Distributed tracing** with OpenTelemetry. Propagate `traceparent` across HTTP, queues, and async hops. - Structured logs with `trace_id` and `span_id` on every line. - Per-service metrics: requests/sec, p99 latency, error rate, queue depth. ## Versioning - API versioning in the URL or header. `v1` is forever; `v2` is additive at first. - Producer compatibility for events: only **add** fields, never remove or rename. Consumers tolerate unknown fields. ## Service mesh / gateway - An API gateway terminates TLS, does auth, and routes. Services trust the gateway's auth claims. - Mesh (Istio, Linkerd) for mutual TLS and traffic policies. Don't reinvent in app code. ## Local development - One docker-compose per service group. New devs `docker compose up`. - Stub external services with **wiremock** or **mockoon**. - Don't run all services on every laptop. Run the ones you change; talk to a shared dev environment for the rest. ## Don't - Don't share databases across services to "save effort". You're trading short-term convenience for long-term coupling. - Don't expose internal IDs across service boundaries. Use stable UUIDs. - Don't synchronously call N services to assemble a response. Aggregate via materialized views or a BFF. - Don't deploy services together as a unit. The whole point is independent rollout.
Have a CLAUDE.md template that works for you?
Send it in — we’ll credit you and publish it under the right tags.