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.
- name
nestjs-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
- You want a minimal Node server without a framework layer — use express-node
- The problem is TypeScript typing itself — use typescript-strict
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.
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:
- Is Z in
providersof this module, or exported by a module listed inimports? - Is Z injected by an interface or type alias? Types vanish at runtime: use a class or
an injection token (
@Inject(PAYMENT_GATEWAY)). - Barrel-file import cycles yield
undefinedat decoration time; import from the file directly. - Is Z a module accidentally placed in
providersinstead ofimports?
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
| Scope | Use when | Cost |
|---|---|---|
DEFAULT (singleton) | Almost always | None |
REQUEST | Truly per-request state that cannot be passed as an argument | Instance per request, and every consumer up the chain becomes request-scoped |
TRANSIENT | Per-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:
- Middleware (global, then module-bound)
- Guards (global, controller, route): return
falseor throw to reject - Interceptors, before
next.handle()(global, controller, route) - Pipes (global, controller, route, then parameter): transform and validate
- Route handler
- Interceptors, after (reverse order, via RxJS operators on the stream)
- Exception filters (route, controller, global): first match wins
| Concern | Enhancer |
|---|---|
| Authentication / authorization / roles | Guard (CanActivate) reading metadata via Reflector |
| DTO validation and type coercion | ValidationPipe |
| Response mapping, timing, caching, timeouts | Interceptor |
| Mapping domain exceptions to HTTP | Exception 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-validatordecorators needclass-transformerandtransform: truefor nested objects; add@ValidateNested()and@Type(() => ItemDto)on nested arrays.- Throwing plain
Erroryields 500; throwNotFoundExceptionetc. or map in a filter. - Long-running work in an interceptor’s
tapstill 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
npm run build && npm run lint && npm test && npm run test:e2epass.- Boot the app (
npm run start) and confirm no DI errors and expected routes in the log. - 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.