Why NestJS Needs Specific AI Rules
NestJS is the most opinionated Node.js framework — it provides dependency injection, a module system, decorators for routing and validation, and built-in patterns for guards, pipes, interceptors, and exception filters. AI assistants ignore all of this, generating Express-style code: manual middleware chains, inline validation, scattered error handling, and no module organization.
The result is NestJS code that works but bypasses every architectural pattern the framework provides. It's like using React without components — technically possible, architecturally wrong. NestJS's patterns exist for testability, modularity, and maintainability. AI code that ignores them loses all three benefits.
These rules target NestJS 10+ with TypeScript. They cover the core framework, common patterns, and integration with TypeORM or Prisma for database access.
Rule 1: Module Organization and Providers
The rule: 'Every feature has its own module: users.module.ts, orders.module.ts, auth.module.ts. Modules declare their controllers, providers (services), imports, and exports. Use @Module() decorator on every module. Import modules — never import services directly from other modules without exporting them. Use the NestJS CLI (nest generate) for scaffolding.'
For providers: 'Services are @Injectable() providers registered in their module. Use constructor injection: constructor(private readonly usersService: UsersService). Never instantiate services with new — always inject. Use custom providers for configuration: { provide: "CONFIG", useValue: config }. Use factory providers for dynamic creation.'
For shared modules: 'Create a SharedModule for providers used across many modules (logging, config, common utilities). Export providers that other modules need. Import SharedModule in every module that uses shared services. Never make everything global — use @Global() only for truly app-wide services (config, logging).'
- One module per feature: users.module.ts, orders.module.ts, auth.module.ts
- @Injectable() on all services — constructor injection, never new
- Export providers for cross-module use — import the module, not the service
- SharedModule for common utilities — @Global() only for app-wide services
- nest generate for scaffolding — CLI ensures consistent file structure
NestJS's DI system manages service lifecycle. new UsersService() bypasses DI — breaking testability, scoping, and lifecycle hooks. Always use constructor injection: constructor(private readonly usersService: UsersService).
Rule 2: Controllers and Routing
The rule: 'Controllers handle HTTP requests — they don't contain business logic. Use decorators for routing: @Controller("users"), @Get(), @Post(), @Put(), @Delete(). Use @Param(), @Query(), @Body() for request data extraction. Controllers call services and return results — validation, auth, and error handling are handled by pipes, guards, and filters.'
For response handling: 'Return values directly — NestJS serializes to JSON automatically. Use @HttpCode() for non-200 status codes. Use class-transformer with @SerializeOptions for response shaping. Use interceptors for consistent response wrapping. Never manually call res.json() — it bypasses NestJS's response pipeline.'
For DTOs: 'Define DTOs (Data Transfer Objects) as classes with class-validator decorators: class CreateUserDto { @IsEmail() email: string; @IsString() @MinLength(1) name: string; }. Use DTOs as @Body() types. The ValidationPipe validates automatically. Never use raw interfaces for request bodies — classes enable runtime validation.'
Rule 3: Guards, Pipes, and Interceptors
The rule: 'Use Guards for authorization: @UseGuards(AuthGuard) on controllers or routes. Guards return true/false — if false, NestJS returns 403. Use Pipes for validation and transformation: @UsePipes(ValidationPipe) or global: app.useGlobalPipes(new ValidationPipe()). Use Interceptors for response transformation, caching, logging, and timeout: @UseInterceptors(LoggingInterceptor).'
For the execution order: 'NestJS executes in order: Middleware → Guards → Interceptors (before) → Pipes → Route Handler → Interceptors (after) → Exception Filters. Understand this order — it determines where logic belongs. Auth check? Guard. Input validation? Pipe. Response wrapping? Interceptor. Error formatting? Exception filter.'
For global vs local: 'Use global pipes for validation (every route validates). Use controller-level guards for auth (entire controller requires auth). Use route-level interceptors for specific transformations. Global applies everywhere, controller-level to all routes in a controller, route-level to one route.'
- Guards: authorization — return true/false — @UseGuards(AuthGuard)
- Pipes: validation + transformation — ValidationPipe global by default
- Interceptors: response wrapping, logging, caching, timeout
- Exception Filters: error formatting — consistent error responses
- Order: Middleware → Guards → Interceptors → Pipes → Handler → Interceptors → Filters
Middleware → Guards → Interceptors (before) → Pipes → Handler → Interceptors (after) → Filters. Auth in Guards, validation in Pipes, response wrapping in Interceptors. Put logic in the right layer.
Rule 4: Database Access Patterns
For TypeORM: 'Define entities with @Entity() decorator. Use @InjectRepository(User) in services for repository access. Use QueryBuilder for complex queries. Use migrations for schema changes — never synchronize: true in production. Use transactions with QueryRunner for multi-step operations.'
For Prisma: 'Use PrismaService extending PrismaClient as an @Injectable() provider. Import PrismaModule in every module that needs database access. Use Prisma's type-safe client for all queries. Define models in schema.prisma. Use prisma migrate for schema changes. Use Prisma's transaction API for atomic operations.'
For either ORM: 'Repository/service layer: services call repositories (TypeORM) or PrismaService (Prisma). Controllers never access the database directly. Keep database operations in the service layer. Use DTOs for input, entities/models for database, and separate response DTOs for output — never expose database models directly in API responses.'
AI returns database entities directly in API responses — exposing internal fields, relations, and metadata. Use separate response DTOs. The entity is for the DB, the DTO is for the API. Never the same class.
Rule 5: Testing with Jest and NestJS Testing Utils
The rule: 'Use NestJS's Test.createTestingModule for unit tests. Mock providers with overrideProvider: module.overrideProvider(UsersService).useValue(mockUsersService). Test controllers independently from services. Test services independently from repositories. Use supertest for e2e testing: import { Test } from "@nestjs/testing"; const app = moduleRef.createNestApplication().'
For test structure: 'Co-locate test files: users.service.spec.ts next to users.service.ts. Use describe blocks per class/method. Mock only external boundaries (database, HTTP clients) — test internal logic with real instances. Use factory functions for test data: createMockUser(), createMockOrder().'
For e2e tests: 'Create a test module that mirrors the real app but with test databases and mocked external services. Test the full request → response cycle with supertest. Verify status codes, response bodies, and database side effects. Clean up test data between tests with database transactions or truncation.'
Complete NestJS Rules Template
Consolidated rules for NestJS projects.
- Module per feature: controllers + providers + imports + exports — nest generate for scaffolding
- Constructor injection: @Injectable() services, never new — SharedModule for common providers
- Controllers: decorators for routing, DTOs for input, services for logic — never res.json()
- Guards for auth, ValidationPipe global, Interceptors for response wrapping
- DTOs with class-validator — classes not interfaces — runtime validation
- TypeORM or Prisma: service layer owns DB access — never DB in controllers
- Entities for DB, DTOs for input/output — never expose DB models in responses
- Test.createTestingModule — overrideProvider for mocks — supertest for e2e