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.

  • Backend & APIs
  • $nestjs-modules
  • 226 lines
  • 2 files
  • ~2,150 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- nestjs-modules

Installs to ~/.agents/skills/nestjs-modules/ for every project on this machine.

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

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.

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

--- name: nestjs-modules description: Structures NestJS 11 applications around feature modules and dependency injection - providers and custom tokens, exports, dynamic modules with… license: MIT ---

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/core major, @nestjs/platform-express or platform-fastify), nest-cli.json (monorepo or standard), and src/main.ts for 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 exports it and the consumer imports the module. Export services, not repositories, unless sharing data access is intentional.
  • Never list another module’s provider in your own providers array 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 orders scaffolds 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) {}
}
NeedProvider form
Swap implementation by environment or in testsuseClass behind a token
Constants, pre-built clientsuseValue
Built from config or other providers, possibly asyncuseFactory + inject
Alias one token to another provideruseExisting

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:

  1. Is the dependency provided in that module or exported by an imported module?
  2. Is the import of the provider a type-only import or a barrel file that creates a cycle (the token becomes undefined at decoration time)?
  3. Does it need @Inject(TOKEN) because it is an interface or a string/symbol token?
  4. 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

ScopeCostUse when
DEFAULT (singleton)NoneAlmost always
REQUESTNew instance per request for it and every provider that injects it, up the chainPer-request state that cannot be passed as an argument
TRANSIENTNew instance per consumerStateful 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).

ConcernEnhancer
Is this caller allowed at all? Roles, API keysGuard
Shape and validate input, transform typesPipe (ValidationPipe)
Logging, timing, response mapping, cachingInterceptor
Mapping exceptions to HTTP/RPC responsesException 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 of forRoutes("*")).
  • 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.
  • useGlobalPipes in main.ts does not apply in e2e tests unless the test applies it.
  • Throwing plain Error yields 500 with a generic body; throw HttpException subclasses or map domain errors in a filter.
  • @Res() in a controller disables Nest’s response handling (interceptors, serialization) unless you pass { passthrough: true }.
  • onModuleInit work 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.yamlgenerated from this page

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.

esc
Just launched

Codex Skills Pro is live

79 premium skills for Codex, each with scripts, references and an evidence report that proves the result.

Bought one by one$882.21
All 79 skills$49
94% off

That's $0.62 a skill, paid once.

Get all 79 for $49Or pick single skills from $4.99 →

Secure checkout by Creem