NestJS Expert
Turns Claude into a senior NestJS backend engineer who ships NestJS 11 / Node 22 services with correct DI scoping, validated config, typed database access, and verified tests.
When to Use This Skill
- Scaffold or extend a NestJS module: controller, service, DTOs, and wiring into the app graph
- Debug DI failures ("Nest can't resolve dependencies of X") or circular module imports
- Add request validation (ValidationPipe + class-validator, or Zod pipes) and typed, validated env config
- Integrate Prisma or TypeORM, including transactions and testcontainer-backed tests
- Implement auth: passport-jwt guards, @nestjs/jwt token issuing, refresh-token rotation
- Add OpenAPI/Swagger documentation that matches the actual DTOs
- Write or fix e2e tests with Test.createTestingModule + supertest, and background jobs with BullMQ
Core Workflow
- Analyze - Read
nest-cli.json, package.json (confirm @nestjs/core major = 11, ORM, validation lib), src/main.ts (global pipes/filters/prefix), and app.module.ts to map the module graph. Match the project's existing conventions (barrel files, DTO location, ConfigService vs registerAs tokens) before writing anything.
- Implement - Write the feature as module + controller + service + DTOs. Default to singleton providers; justify any
Scope.REQUEST explicitly (see references/architecture-di.md). New cross-cutting behavior goes in guards/interceptors/filters, not in controllers.
- Verify types and lint - run
npx tsc --noEmit and npm run lint; fix all reported issues and re-run until clean before proceeding.
- Test - add/update unit tests for services and e2e tests for new endpoints; run
npm test and npm run test:e2e; fix all failures and re-run until clean. Never weaken an assertion to make a test pass.
- Prove it works - start the app (
npm run start:dev) and hit the new endpoint with curl, including one invalid payload to confirm the 400 shape from ValidationPipe. For queues, enqueue a job and confirm the processor log. Fix anything wrong and re-verify until the observed behavior matches the requirement.
Reference Guide
Load detailed guidance only when the task needs it:
| Topic |
Reference |
Load When |
| Modules, DI scopes, providers, lifecycle, execution order |
references/architecture-di.md |
Creating modules, DI resolution errors, request-scoped providers, guard/interceptor/filter ordering questions |
| ValidationPipe, class-validator vs Zod, ConfigModule + env validation, OpenAPI decorators |
references/validation-config.md |
Adding DTO validation, choosing a validation library, wiring .env config, "config is undefined at bootstrap", Swagger/OpenAPI docs |
| Prisma and TypeORM integration, transactions, migrations |
references/database-prisma.md |
Any DB work: schema changes, repositories, transactions, connection lifecycle, choosing Prisma vs TypeORM |
| Passport-jwt vs @nestjs/jwt, refresh-token rotation, route guarding |
references/auth.md |
Login/JWT endpoints, protecting routes, refresh flows, "401 on valid token" debugging |
| Unit + e2e testing, supertest, overriding providers, BullMQ jobs |
references/testing.md |
Writing tests, mocking providers, e2e app bootstrap parity, queue/worker implementation and testing |
Key Patterns
Singleton by default; request scope is a measured exception. Scope.REQUEST bubbles: every consumer of a request-scoped provider becomes request-scoped and is re-instantiated per request. For "who is the current user" style needs, prefer AsyncLocalStorage (via nestjs-cls) or pass context explicitly:
@Injectable() // singleton - no scope option
export class AuditService {
constructor(private readonly cls: ClsService) {}
log(action: string) {
this.cls.get('userId'); // per-request data without per-request instantiation
}
}
Global ValidationPipe with the three flags that matter (in main.ts):
app.useGlobalPipes(new ValidationPipe({
whitelist: true, // strip unknown properties
forbidNonWhitelisted: true, // 400 on unknown properties
transform: true, // plain body -> DTO instance, enables @Type coercion
}));
Custom providers use tokens, not strings scattered inline:
export const REDIS = Symbol('REDIS');
@Module({
providers: [{ provide: REDIS, useFactory: (cfg: ConfigService) => new Redis(cfg.getOrThrow('REDIS_URL')), inject: [ConfigService] }],
exports: [REDIS],
})
export class RedisModule {}
// consumer: constructor(@Inject(REDIS) private readonly redis: Redis) {}
e2e tests must mirror production bootstrap - apply the same global pipes/filters the real main.ts applies, or e2e tests pass while production rejects:
const moduleRef = await Test.createTestingModule({ imports: [AppModule] }).compile();
app = moduleRef.createNestApplication();
applyGlobalConfig(app); // shared helper also called from main.ts
await app.init();
Common Mistakes
- Marking providers
Scope.REQUEST for convenience. It cascades to every dependent, kills singleton caching, and breaks in passive contexts (cron, queue processors). Correction: stay singleton; use nestjs-cls/AsyncLocalStorage for per-request data, or moduleRef.resolve() with a ContextId when you truly need per-request instances.
- Assuming providers are visible app-wide. A provider is injectable only inside its module unless
exports-ed and the consumer imports that module. "Nest can't resolve dependencies" is almost always a missing export/import, not a missing @Injectable().
- Wrong mental model of execution order. Request path: middleware → guards → interceptors (pre) → pipes → handler → interceptors (post, reverse registration order) → exception filters. Guards run before interceptors, so an interceptor cannot run for a request a guard rejected; filters catch errors from all of the preceding stages except middleware.
- ValidationPipe without
whitelist/transform. Default config lets unknown properties through (mass-assignment risk) and leaves @Query() params as strings. Also: class-validator decorators do nothing on interfaces or on properties without decorators - every DTO field needs one (or @Allow()).
- Reading
process.env directly in providers. Bypasses ConfigModule validation and load order; values are string | undefined at type level and unvalidated at runtime. Correction: validate env at bootstrap (Joi/Zod validate in ConfigModule.forRoot) and inject ConfigService with getOrThrow, or typed registerAs namespaces.
- Passport confusion:
@nestjs/jwt and passport-jwt are not alternatives for the same job. @nestjs/jwt signs/verifies tokens (issuing side); passport-jwt + JwtStrategy + AuthGuard('jwt') validates them on requests. A typical app uses both; implementing verification manually in a guard while also registering a strategy leads to double/contradictory validation.
- Storing raw refresh tokens, or not rotating them. Store only a hash, rotate on every refresh, and revoke the family on reuse detection. See
references/auth.md.
@Body() body: any plus manual checks instead of DTOs. Kills ValidationPipe, OpenAPI generation (@nestjs/swagger CLI plugin reads DTO classes), and type safety in one move.
1---2name: nestjs-expert3description: Use when working in a NestJS project - nest-cli.json, @nestjs/* imports, *.module.ts / *.controller.ts / *.service.ts files, main.ts with NestFactory, or tasks mentioning NestJS, Nest modules, providers, guards, interceptors, pipes, or BullMQ workers. Builds and refactors NestJS 11 APIs, wires DI and config, integrates Prisma/TypeORM, implements JWT auth and refresh flows, and writes e2e tests. Invoke for creating modules/endpoints, fixing DI errors, adding validation, auth, OpenAPI docs, queues, or e2e test suites.4license: MIT5---67# NestJS Expert89Turns Claude into a senior NestJS backend engineer who ships NestJS 11 / Node 22 services with correct DI scoping, validated config, typed database access, and verified tests.1011## When to Use This Skill1213- Scaffold or extend a NestJS module: controller, service, DTOs, and wiring into the app graph14- Debug DI failures ("Nest can't resolve dependencies of X") or circular module imports15- Add request validation (ValidationPipe + class-validator, or Zod pipes) and typed, validated env config16- Integrate Prisma or TypeORM, including transactions and testcontainer-backed tests17- Implement auth: passport-jwt guards, @nestjs/jwt token issuing, refresh-token rotation18- Add OpenAPI/Swagger documentation that matches the actual DTOs19- Write or fix e2e tests with Test.createTestingModule + supertest, and background jobs with BullMQ2021## Core Workflow22231. **Analyze** - Read `nest-cli.json`, `package.json` (confirm `@nestjs/core` major = 11, ORM, validation lib), `src/main.ts` (global pipes/filters/prefix), and `app.module.ts` to map the module graph. Match the project's existing conventions (barrel files, DTO location, `ConfigService` vs `registerAs` tokens) before writing anything.242. **Implement** - Write the feature as module + controller + service + DTOs. Default to singleton providers; justify any `Scope.REQUEST` explicitly (see `references/architecture-di.md`). New cross-cutting behavior goes in guards/interceptors/filters, not in controllers.253. **Verify types and lint** - run `npx tsc --noEmit` and `npm run lint`; fix all reported issues and re-run until clean before proceeding.264. **Test** - add/update unit tests for services and e2e tests for new endpoints; run `npm test` and `npm run test:e2e`; fix all failures and re-run until clean. Never weaken an assertion to make a test pass.275. **Prove it works** - start the app (`npm run start:dev`) and hit the new endpoint with `curl`, including one invalid payload to confirm the 400 shape from ValidationPipe. For queues, enqueue a job and confirm the processor log. Fix anything wrong and re-verify until the observed behavior matches the requirement.2829## Reference Guide3031Load detailed guidance only when the task needs it:3233| Topic | Reference | Load When |34|-------|-----------|-----------|35| Modules, DI scopes, providers, lifecycle, execution order | `references/architecture-di.md` | Creating modules, DI resolution errors, request-scoped providers, guard/interceptor/filter ordering questions |36| ValidationPipe, class-validator vs Zod, ConfigModule + env validation, OpenAPI decorators | `references/validation-config.md` | Adding DTO validation, choosing a validation library, wiring `.env` config, "config is undefined at bootstrap", Swagger/OpenAPI docs |37| Prisma and TypeORM integration, transactions, migrations | `references/database-prisma.md` | Any DB work: schema changes, repositories, transactions, connection lifecycle, choosing Prisma vs TypeORM |38| Passport-jwt vs @nestjs/jwt, refresh-token rotation, route guarding | `references/auth.md` | Login/JWT endpoints, protecting routes, refresh flows, "401 on valid token" debugging |39| Unit + e2e testing, supertest, overriding providers, BullMQ jobs | `references/testing.md` | Writing tests, mocking providers, e2e app bootstrap parity, queue/worker implementation and testing |4041## Key Patterns4243**Singleton by default; request scope is a measured exception.** `Scope.REQUEST` bubbles: every consumer of a request-scoped provider becomes request-scoped and is re-instantiated per request. For "who is the current user" style needs, prefer `AsyncLocalStorage` (via `nestjs-cls`) or pass context explicitly:4445```typescript46@Injectable() // singleton - no scope option47export class AuditService {48 constructor(private readonly cls: ClsService) {}49 log(action: string) {50 this.cls.get('userId'); // per-request data without per-request instantiation51 }52}53```5455**Global ValidationPipe with the three flags that matter** (in `main.ts`):5657```typescript58app.useGlobalPipes(new ValidationPipe({59 whitelist: true, // strip unknown properties60 forbidNonWhitelisted: true, // 400 on unknown properties61 transform: true, // plain body -> DTO instance, enables @Type coercion62}));63```6465**Custom providers use tokens, not strings scattered inline:**6667```typescript68export const REDIS = Symbol('REDIS');6970@Module({71 providers: [{ provide: REDIS, useFactory: (cfg: ConfigService) => new Redis(cfg.getOrThrow('REDIS_URL')), inject: [ConfigService] }],72 exports: [REDIS],73})74export class RedisModule {}75// consumer: constructor(@Inject(REDIS) private readonly redis: Redis) {}76```7778**e2e tests must mirror production bootstrap** - apply the same global pipes/filters the real `main.ts` applies, or e2e tests pass while production rejects:7980```typescript81const moduleRef = await Test.createTestingModule({ imports: [AppModule] }).compile();82app = moduleRef.createNestApplication();83applyGlobalConfig(app); // shared helper also called from main.ts84await app.init();85```8687## Common Mistakes8889- **Marking providers `Scope.REQUEST` for convenience.** It cascades to every dependent, kills singleton caching, and breaks in passive contexts (cron, queue processors). Correction: stay singleton; use `nestjs-cls`/`AsyncLocalStorage` for per-request data, or `moduleRef.resolve()` with a `ContextId` when you truly need per-request instances.90- **Assuming providers are visible app-wide.** A provider is injectable only inside its module unless `exports`-ed and the consumer `imports` that module. "Nest can't resolve dependencies" is almost always a missing export/import, not a missing `@Injectable()`.91- **Wrong mental model of execution order.** Request path: middleware → guards → interceptors (pre) → pipes → handler → interceptors (post, reverse registration order) → exception filters. Guards run *before* interceptors, so an interceptor cannot run for a request a guard rejected; filters catch errors from all of the preceding stages except middleware.92- **ValidationPipe without `whitelist`/`transform`.** Default config lets unknown properties through (mass-assignment risk) and leaves `@Query()` params as strings. Also: class-validator decorators do nothing on `interface`s or on properties without decorators - every DTO field needs one (or `@Allow()`).93- **Reading `process.env` directly in providers.** Bypasses ConfigModule validation and load order; values are `string | undefined` at type level and unvalidated at runtime. Correction: validate env at bootstrap (Joi/Zod `validate` in `ConfigModule.forRoot`) and inject `ConfigService` with `getOrThrow`, or typed `registerAs` namespaces.94- **Passport confusion: `@nestjs/jwt` and `passport-jwt` are not alternatives for the same job.** `@nestjs/jwt` signs/verifies tokens (issuing side); `passport-jwt` + `JwtStrategy` + `AuthGuard('jwt')` validates them on requests. A typical app uses both; implementing verification manually in a guard while also registering a strategy leads to double/contradictory validation.95- **Storing raw refresh tokens, or not rotating them.** Store only a hash, rotate on every refresh, and revoke the family on reuse detection. See `references/auth.md`.96- **`@Body() body: any` plus manual checks instead of DTOs.** Kills ValidationPipe, OpenAPI generation (`@nestjs/swagger` CLI plugin reads DTO classes), and type safety in one move.