NestJS Modules skill for Codex
Clean module graphs and DI that tests can swap. Codex organises NestJS code into feature modules with deliberate exports, injects external services behind symbol tokens, and builds configurable infrastructure with ConfigurableModuleBuilder. It diagnoses dependency resolution errors in a fixed order, avoids request scope unless required, puts each cross-cutting concern in the correct enhancer, and proves the graph with testing-module overrides.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- nestjs-modules Installs to ~/.agents/skills/nestjs-modules/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install nestjs-modules The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project nestjs-modules Installs to .agents/skills/nestjs-modules/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/nestjs-modules.zipunzip -o nestjs-modules.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) nestjs-modules PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $nestjs-modules, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Structures NestJS 11 applications around feature modules and dependency injection - providers and custom tokens, exports, dynamic modules with forRoot/forRootAsync, request scope costs, circular dependency fixes, guards with Reflector metadata, ValidationPipe with DTOs, interceptors, exception filters and Test.createTestingModule overrides. Use when adding a NestJS feature module, fixing "Nest can't resolve dependencies" errors, wiring auth guards or config, or reviewing a Nest codebase for module boundaries.
Use it when
- Adding feature modules, providers or dynamic infrastructure modules in NestJS
- Debugging dependency resolution or circular dependency errors
- Wiring global auth guards, validation pipes, interceptors or exception filters
- Upgrading a Nest app to version 11
Use something else when
- You want a minimal Node API without a DI container: use fastify-api
- The question is about general DI patterns outside Nest: use dependency-injection
How to check the result
The app boots with no resolution errors, unit tests override external providers through their tokens, and e2e tests show unauthenticated requests get 401 and unknown body fields get 400.
The mistake it prevents
Adding another module's service to your own providers array to silence a DI error, which quietly creates a second instance with separate state.
Done well means: each feature is a module that exports only what other modules need, providers depend on abstractions injected by token, cross-cutting concerns live in the right enhancer (guard, pipe, interceptor, filter), request scope is used only where it is unavoidable, and every provider can be replaced in a testing module.
Inspect first
- Read
AGENTS.md,package.json(@nestjs/coremajor,@nestjs/platform-expressorplatform-fastify),nest-cli.json(monorepo or standard), andsrc/main.tsfor global pipes, filters, versioning and shutdown hooks. - Draw the module graph before adding to it:
rg -n "@Module\(" src -A 8. - Find global enhancers:
rg -n "APP_GUARD|APP_PIPE|APP_INTERCEPTOR|APP_FILTER|useGlobal" src.
Module design rules
src/
app.module.ts
config/ # ConfigModule setup and typed config
database/ # DatabaseModule (forRootAsync), exports repositories or a client
orders/
orders.module.ts
orders.controller.ts
orders.service.ts
dto/create-order.dto.ts
orders.repository.ts
common/ # guards, decorators, filters, interceptors (no business logic)
- A provider is visible only inside its module unless the module
exportsit and the consumerimportsthe module. Export services, not repositories, unless sharing data access is intentional. - Never list another module’s provider in your own
providersarray to “fix” a resolution error: that creates a second instance with its own state. - Use
@Global()only for true infrastructure (config, logger). Global feature modules hide dependencies and make the graph unreadable. nest g resource ordersscaffolds module, controller, service and DTOs; review and trim what it generates.
Providers and tokens
export const PAYMENT_GATEWAY = Symbol("PAYMENT_GATEWAY");
export interface PaymentGateway { charge(amountCents: number, ref: string): Promise<string>; }
@Module({
providers: [
OrdersService,
{ provide: PAYMENT_GATEWAY, useClass: StripeGateway },
{
provide: "ORDERS_CONFIG",
inject: [ConfigService],
useFactory: (cfg: ConfigService) => ({ maxQty: cfg.getOrThrow<number>("ORDERS_MAX_QTY") }),
},
],
exports: [OrdersService],
})
export class OrdersModule {}
@Injectable()
export class OrdersService {
constructor(@Inject(PAYMENT_GATEWAY) private readonly payments: PaymentGateway) {}
}
| Need | Provider form |
|---|---|
| Swap implementation by environment or in tests | useClass behind a token |
| Constants, pre-built clients | useValue |
| Built from config or other providers, possibly async | useFactory + inject |
| Alias one token to another provider | useExisting |
TypeScript interfaces vanish at runtime, so injecting by interface type needs a token.
Reading “Nest can’t resolve dependencies”
The error names the class, the argument index and the module. Check in order:
- Is the dependency provided in that module or exported by an imported module?
- Is the import of the provider a type-only import or a barrel file that creates a
cycle (the token becomes
undefinedat decoration time)? - Does it need
@Inject(TOKEN)because it is an interface or a string/symbol token? - Is it a true circular dependency between modules or providers?
Fix cycles by moving the shared logic into a third module first. Use
forwardRef(() => OtherModule) (in imports) and @Inject(forwardRef(() => OtherService))
only when the cycle is genuine and small.
Dynamic modules
For configurable infrastructure, use ConfigurableModuleBuilder instead of hand-written
forRoot boilerplate:
export const { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN } =
new ConfigurableModuleBuilder<{ url: string }>().setClassMethodName("forRoot").build();
@Module({ providers: [CacheClient], exports: [CacheClient] })
export class CacheModule extends ConfigurableModuleClass {}
// app.module.ts
CacheModule.forRootAsync({ inject: [ConfigService], useFactory: (c: ConfigService) => ({ url: c.getOrThrow("REDIS_URL") }) })
Config: ConfigModule.forRoot({ isGlobal: true, validate }) with a Zod or class-validator
schema so a missing variable fails at boot, not on the first request.
Scopes
| Scope | Cost | Use when |
|---|---|---|
DEFAULT (singleton) | None | Almost always |
REQUEST | New instance per request for it and every provider that injects it, up the chain | Per-request state that cannot be passed as an argument |
TRANSIENT | New instance per consumer | Stateful helpers like per-class loggers |
Request scope bubbles up: one request-scoped repository makes its service and controller
request-scoped too. Prefer passing tenant/user explicitly, or AsyncLocalStorage (for
example nestjs-cls), over REQUEST scope.
Request pipeline
Order: middleware -> guards -> interceptors (before) -> pipes -> handler -> interceptors (after) -> exception filters (on error).
| Concern | Enhancer |
|---|---|
| Is this caller allowed at all? Roles, API keys | Guard |
| Shape and validate input, transform types | Pipe (ValidationPipe) |
| Logging, timing, response mapping, caching | Interceptor |
| Mapping exceptions to HTTP/RPC responses | Exception filter |
| Raw request tweaks before routing (rare) | Middleware |
// main.ts
app.useGlobalPipes(new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true, transform: true }));
app.enableShutdownHooks();
Register guards that need DI globally through a provider, not useGlobalGuards(new X()):
export const Roles = Reflector.createDecorator<string[]>();
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private readonly reflector: Reflector) {}
canActivate(ctx: ExecutionContext): boolean {
const roles = this.reflector.getAllAndOverride(Roles, [ctx.getHandler(), ctx.getClass()]);
if (!roles?.length) return true;
const user = ctx.switchToHttp().getRequest().user;
return roles.some((r) => user?.roles?.includes(r));
}
}
// app.module.ts providers
{ provide: APP_GUARD, useClass: JwtAuthGuard },
{ provide: APP_GUARD, useClass: RolesGuard },
With a global auth guard, mark public routes with a @Public() metadata decorator that
the auth guard checks, so new routes are protected by default.
DTOs: classes with class-validator decorators (@IsString(), @IsInt(), @Min(1)).
Without whitelist: true, extra body fields pass straight into your services.
Testing
const moduleRef = await Test.createTestingModule({ imports: [OrdersModule] })
.overrideProvider(PAYMENT_GATEWAY).useValue({ charge: vi.fn().mockResolvedValue("ch_1") })
.compile();
const service = moduleRef.get(OrdersService);
For HTTP tests, moduleRef.createNestApplication(), apply the same global pipes as
main.ts (extract a configureApp(app) function so they cannot drift), await app.init(),
then use Supertest against app.getHttpServer().
Nest 11 notes
- Express 5 is the default HTTP adapter: wildcard routes and middleware paths need named
wildcards (
forRoutes("{*splat}")instead offorRoutes("*")). - Check third-party
@nestjs/*packages are on matching majors after upgrading; mixed majors produce confusing DI errors.
Verification
npx tsc --noEmit
npm test
npm run start:dev # boot proves the DI graph resolves; watch for resolution errors
Then confirm: a request without credentials to a protected route returns 401, an extra
body field returns 400 (with forbidNonWhitelisted), and the unit test replaces the
external gateway via its token.
Gotchas
import type { X }for a class used as an injection token erases it; use a value import.useGlobalPipesinmain.tsdoes not apply in e2e tests unless the test applies it.- Throwing plain
Erroryields 500 with a generic body; throwHttpExceptionsubclasses or map domain errors in a filter. @Res()in a controller disables Nest’s response handling (interceptors, serialization) unless you pass{ passthrough: true }.onModuleInitwork that calls external services slows boot and breaks tests; inject and mock those clients.
Output template
## Nest change: <summary>
- Modules: <added/changed, imports, exports>
- Providers/tokens: <token -> implementation>
- Enhancers: <guards/pipes/interceptors/filters and scope (global or route)>
- Scope changes: <none | request-scoped X because ...>
- Tests: <testing module overrides, e2e cases> What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "NestJS Modules"
short_description: "Clean module graphs and DI that tests can swap"
default_prompt: "$nestjs-modules Add a payments module to our NestJS app with the gateway injected by token" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.