Backend & APIs No. 090

NestJS Modules skill for Claude Code

Clear module boundaries and enhancers in the right place. Claude organizes NestJS code into feature modules with small export lists, uses injection tokens instead of interfaces that vanish at runtime, and avoids request scope unless it is truly needed. Auth goes in guards, validation in a global ValidationPipe, mapping in interceptors, and tests override providers through the testing module so e2e runs use the same global config as production.

  • namenestjs-modules
  • size172 lines · ≈1,800 tokens
  • files1
  • licenseMIT
  • updated

What Claude sees before loading it

This description is the only part of the skill that sits in context all the time. Claude reads it to decide when to load the full playbook below.

description: Structures NestJS applications - feature modules, providers and dependency injection scopes, custom and async providers, dynamic modules, configuration, guards, pipes, interceptors and exception filters in the right lifecycle order, ValidationPipe with DTOs, and unit and e2e tests with the testing module. Use when creating or refactoring NestJS modules, resolving circular dependency or "Nest can't resolve dependencies" errors, adding auth guards or global validation, or writing NestJS tests.

Try asking

  • Nest can't resolve dependencies of OrdersService. Fix it properly
  • Add a global JWT guard with a @Public() opt-out to this NestJS app
  • Write unit and e2e tests for the payments module

Use it when

  • Creating or refactoring NestJS modules and providers
  • Hitting DI resolution or circular dependency errors
  • Adding guards, pipes, interceptors or exception filters
  • Writing NestJS unit or e2e tests

Skip it when

How to check the result

Build, lint, unit and e2e tests pass, the app boots without DI errors, and e2e checks confirm 401, 400 and 404 responses use the standard error body.

The common mistake it prevents

Registering global pipes in main.ts but not in the e2e test app, so tests pass while validation is never exercised.

~/.claude/skills/nestjs-modules/SKILL.md

--- name: nestjs-modules description: Structures NestJS applications - feature modules, providers and dependency injection scopes, custom and async providers, dynamic modules,… license: MIT ---

A well-built Nest app has one module per feature with a deliberately small exports list, singleton providers by default, cross-cutting concerns in the matching enhancer (guard for authz, pipe for validation, interceptor for mapping, filter for errors), and tests that swap providers through the testing module instead of patching imports.

Workflow

- [ ] 1. Read package.json (@nestjs/* versions, platform express/fastify) and main.ts
- [ ] 2. Map the module graph: AppModule imports, global modules, exports
- [ ] 3. Place new code in a feature module (nest g resource <name> scaffolds it)
- [ ] 4. Register providers; export only what other modules need
- [ ] 5. Add enhancers at the narrowest scope that works (method < controller < global)
- [ ] 6. Write unit tests (Test.createTestingModule) and e2e tests (supertest)
- [ ] 7. Run build, lint, tests; boot the app to catch DI errors

NestJS 11 (2025) requires Node 20+, uses Express 5 by default (route wildcard syntax changed: *splat instead of *), and improved startup for large module graphs.

Module rules

@Module({
  imports: [TypeOrmModule.forFeature([Order]), PaymentsModule],
  controllers: [OrdersController],
  providers: [OrdersService, OrdersRepository],
  exports: [OrdersService],          // the module's public API; repository stays private
})
export class OrdersModule {}
  • A provider is visible only inside its module unless exported, and only to modules that import the exporting module.
  • @Global() only for true infrastructure (config, logger, database). A global feature module hides coupling.
  • Shared utilities without state can be plain functions; not everything is a provider.

Resolving DI errors

“Nest can’t resolve dependencies of the X (?, Y). Please make sure that the argument Z at index [0] is available in the OrdersModule context.” Check in order:

  1. Is Z in providers of this module, or exported by a module listed in imports?
  2. Is Z injected by an interface or type alias? Types vanish at runtime: use a class or an injection token (@Inject(PAYMENT_GATEWAY)).
  3. Barrel-file import cycles yield undefined at decoration time; import from the file directly.
  4. Is Z a module accidentally placed in providers instead of imports?

Circular module dependencies: first try to extract the shared piece into a third module. forwardRef(() => OtherModule) on both sides is the last resort and a design smell.

Custom providers

export const PAYMENT_GATEWAY = Symbol("PAYMENT_GATEWAY");

providers: [
  { provide: PAYMENT_GATEWAY, useClass: process.env.NODE_ENV === "test" ? FakeGateway : StripeGateway },
  {
    provide: "PG_POOL",
    inject: [ConfigService],
    useFactory: async (config: ConfigService) => {
      const pool = new Pool({ connectionString: config.getOrThrow("DATABASE_URL") });
      await pool.query("select 1");
      return pool;
    },
  },
  { provide: Clock, useValue: systemClock },
]

Async factories block app bootstrap until resolved; keep them fast and fail loudly. Close resources in onModuleDestroy/beforeApplicationShutdown and call app.enableShutdownHooks() in main.ts so SIGTERM triggers them.

Scopes

ScopeUse whenCost
DEFAULT (singleton)Almost alwaysNone
REQUESTTruly per-request state that cannot be passed as an argumentInstance per request, and every consumer up the chain becomes request-scoped
TRANSIENTPer-consumer instance (e.g. a logger with context)Instance per injection

Prefer passing request data as arguments or using AsyncLocalStorage (e.g. nestjs-cls) over REQUEST scope; request scope bubbling quietly multiplies allocations.

Dynamic modules and config

@Module({ imports: [ConfigModule.forRoot({ isGlobal: true, validate: validateEnv, cache: true })] })
export class AppModule {}

Validate env at boot (zod or class-validator in validate); use registerAs("db", () => ({...})) for namespaced config. For your own reusable modules, build forRoot/forRootAsync with ConfigurableModuleBuilder instead of hand-writing dynamic module boilerplate.

Request lifecycle

Order for each request:

  1. Middleware (global, then module-bound)
  2. Guards (global, controller, route): return false or throw to reject
  3. Interceptors, before next.handle() (global, controller, route)
  4. Pipes (global, controller, route, then parameter): transform and validate
  5. Route handler
  6. Interceptors, after (reverse order, via RxJS operators on the stream)
  7. Exception filters (route, controller, global): first match wins
ConcernEnhancer
Authentication / authorization / rolesGuard (CanActivate) reading metadata via Reflector
DTO validation and type coercionValidationPipe
Response mapping, timing, caching, timeoutsInterceptor
Mapping domain exceptions to HTTPException filter, or throw HttpException subclasses
Raw request concerns (body parsing, CORS, request IDs)Middleware

Global validation in main.ts:

app.useGlobalPipes(new ValidationPipe({
  whitelist: true,              // strip properties without decorators
  forbidNonWhitelisted: true,   // or reject them with 400
  transform: true,              // plain object -> DTO instance, "1" -> 1 for typed params
}));

Globals registered with app.useGlobal* cannot inject dependencies; register as { provide: APP_GUARD, useClass: JwtAuthGuard } in a module when they need DI. Combine a global auth guard with a @Public() metadata decorator for opt-out routes.

Tests

const moduleRef = await Test.createTestingModule({
  providers: [OrdersService, { provide: OrdersRepository, useValue: { findById: jest.fn() } }],
}).compile();
const service = moduleRef.get(OrdersService);

E2E: build the real AppModule, override infrastructure with .overrideProvider(X).useValue(fake) or .overrideGuard(JwtAuthGuard).useValue({ canActivate: () => true }), then app = moduleRef.createNestApplication(); applyGlobalConfig(app); await app.init(); and hit it with supertest(app.getHttpServer()). Share the same function to apply global pipes/filters in main.ts and tests, or e2e tests run without validation.

Gotchas

  • Interface-typed constructor params compile but fail at runtime injection.
  • class-validator decorators need class-transformer and transform: true for nested objects; add @ValidateNested() and @Type(() => ItemDto) on nested arrays.
  • Throwing plain Error yields 500; throw NotFoundException etc. or map in a filter.
  • Long-running work in an interceptor’s tap still delays the response stream; offload to a queue (@nestjs/bullmq).
  • Fastify adapter differences: middleware receives raw Node objects; some Express middleware will not work.

Verification

  1. npm run build && npm run lint && npm test && npm run test:e2e pass.
  2. Boot the app (npm run start) and confirm no DI errors and expected routes in the log.
  3. E2E tests prove: unauthenticated request gets 401, extra DTO properties get 400, unknown resource gets 404 with the standard error body.

About this skill

Written and maintained by the Claude Skills editors to the same editorial standard as every skill in the directory: a selective trigger, a real workflow, known pitfalls and a verification step. First published , last updated . MIT licensed.

esc