NestJS is opinionated by design — modules, decorators, dependency injection, providers. But Claude Code doesn't know which patterns you want when there are five ways to do the same thing. Without a CLAUDE.md, it'll mix patterns across modules, forget to use your custom interceptors, and generate services that bypass your validation layer entirely.
Here are 13 rules that fix that.
Rule 1: Module boundaries are inviolable
Every feature lives in its own module. No cross-module imports of services —
use the module's exports. Shared utilities go in a SharedModule. Never import
a repository from another module directly.
Claude defaults to flat structure. This forces it to think in NestJS module boundaries.
Rule 2: Use class-validator on every DTO
All request bodies use DTOs decorated with class-validator. No raw body access
(req.body). ValidationPipe is global. Whitelist: true. ForbidNonWhitelisted: true.
Without this, Claude will generate controllers that accept any on request bodies and skip validation entirely.
Rule 3: Repository pattern — TypeORM or Prisma, never both
Stack: [TypeORM / Prisma — pick one]. All DB access goes through repository
classes injected via DI. No direct EntityManager calls in services.
No raw SQL except in named query methods with explicit typing.
Specify your ORM or Claude will mix them within the same feature.
Rule 4: Interceptors handle transformation, not services
Response transformation (serialization, envelope wrapping) lives in interceptors.
Services return domain objects. Controllers return service output directly —
the interceptor handles the rest.
Claude tends to add toResponse() methods everywhere. This keeps transformation in one layer.
Rule 5: Guards for auth, not middleware
Authentication and authorization use Guards, not middleware. JwtAuthGuard is
applied via @UseGuards(). No manual JWT decoding in controllers or services.
CurrentUser decorator extracts user from request.
Without this rule, Claude scatters auth logic across middleware, controllers, and services.
Rule 6: Exception filters catch everything
A GlobalExceptionFilter is registered in main.ts. Services throw domain
exceptions (NotFoundException, ForbiddenException, etc.) — never format
error responses manually. No try/catch in controllers.
Claude writes try/catch blocks in controllers by default. This eliminates them.
Rule 7: Config via ConfigService, never process.env directly
All environment variables accessed through ConfigService. No direct
process.env references outside config modules. ConfigModule is global.
Validation schema defined with Joi or zod.
Scattered process.env.DATABASE_URL calls throughout the codebase break this rule.
Rule 8: Async providers and circular dependencies
No circular dependencies between modules. Use forwardRef() only when
genuinely unavoidable — document why. Prefer event-based decoupling
(EventEmitter2) over circular injection.
Claude sometimes creates circular imports between services. This makes it flag the problem instead.
Rule 9: Queue patterns for side effects
Side effects (emails, webhooks, analytics) go through Bull queues — never
called synchronously from request handlers. Processors live in dedicated
processor classes, not inline in services.
Synchronous side effects in request paths fail under load. This rule keeps them async.
Rule 10: Swagger documentation is not optional
All controllers decorated with @ApiTags(). All DTOs decorated with @ApiProperty().
@ApiResponse() on all non-200 responses. SwaggerModule configured in main.ts.
Never skip swagger decorators on new endpoints.
Without explicit instruction, Claude treats Swagger as optional. This makes it mandatory.
Rule 11: Testing: unit tests mock providers, e2e tests use real modules
Unit tests use Test.createTestingModule() with all dependencies mocked.
E2e tests use the full application module (no mocks). Test files colocated
with the feature module: feature.service.spec.ts, feature.e2e-spec.ts.
Claude mixes these patterns. Explicit separation prevents false-passing tests.
Rule 12: Logging via Logger, not console
Use NestJS Logger class injected in each service: private readonly logger =
new Logger(ServiceName.name). No console.log. Log context always includes
service name. Errors logged with stack traces.
Console logs don't carry context and don't integrate with log aggregators.
Rule 13: Lifecycle hooks for cleanup
Services that hold connections implement OnModuleInit and OnModuleDestroy.
No open handles in tests — verify with --detectOpenHandles.
Redis, queue, and DB connections closed in onModuleDestroy.
Leaked connections in tests and production shutdown are a common NestJS footgun.
The CLAUDE.md for NestJS (copy this)
# CLAUDE.md
## Stack
- Framework: NestJS (latest stable)
- ORM: [TypeORM / Prisma]
- Queue: Bull + Redis
- Validation: class-validator + class-transformer
- Auth: Passport + JWT
## Architecture rules
- Module boundaries are inviolable — no cross-module service imports
- All request bodies use DTOs with class-validator decorators
- ValidationPipe is global: whitelist: true, forbidNonWhitelisted: true
- Repository pattern for all DB access — no raw queries except named methods
- Response transformation lives in interceptors, not services
- Guards for auth — never middleware or manual JWT decoding
- GlobalExceptionFilter handles all errors — no try/catch in controllers
- ConfigService only — no direct process.env access
- Side effects (email, webhook, analytics) go through Bull queues
- Swagger decorators are mandatory on all controllers and DTOs
- Unit tests mock all providers; e2e tests use full module
- Logger class in every service — no console.log
- OnModuleInit/OnModuleDestroy for all connection-holding services
## File conventions
- feature/feature.module.ts
- feature/feature.controller.ts
- feature/feature.service.ts
- feature/dto/create-feature.dto.ts
- feature/entities/feature.entity.ts
- feature/feature.service.spec.ts
## What's forbidden
- Circular module dependencies (use EventEmitter2 instead)
- Raw process.env outside config modules
- console.log anywhere
- try/catch in controllers
- Direct EntityManager calls in services
Why this matters more in NestJS than other frameworks
NestJS has the richest decorator ecosystem of any Node.js framework. That's a strength — but it means there are more ways for AI to make the wrong choice. Without explicit constraints, Claude will:
- Mix
@Injectable()providers with plain classes - Use
@Res()decorator and bypass NestJS response handling - Write interceptors as middleware
- Skip module registration for new providers
The CLAUDE.md doesn't fight NestJS conventions — it enforces them so every module Claude generates looks like it was written by the same person.
Part of a series: CLAUDE.md files for Go, Rust, TypeScript/Node.js, Python, Java, C#/.NET, PHP, Ruby, Elixir, Scala, Haskell, C++, Vue.js/Nuxt, React/Next.js, Flutter/Dart, Swift/iOS, Spring Boot, Django/FastAPI, Android/Jetpack Compose, and now NestJS.
The full rules pack (all frameworks, team license, setup sprint) is at oliviacraft.lat
Top comments (0)