mcpbeat Sign in

Nestjs Agent Skill

Use when building or structuring a NestJS backend — feature modules, providers and DI wiring, provider scopes and request-lifecycle order, where to bind guards/pipes/interceptors/filters, and testing with Test.createTestingModule. NOT a bare Express/Fastify service with no DI (that is `nodejs`), NOT framework-agnostic REST design (that is `api-design`).

6k tokens
context cost
the whole folder, loaded on every use
6
files
ships runnable scripts
0
copies elsewhere
how many repositories repackaged it
105
stars on the repo
on the repository, not the skill itself

Install

one command, takes just this skill from the repository
npx skills add https://github.com/ericrisco/rsc-harness --skill nestjs

What comes with it

13 853 bytes besides the instruction
evals/README.md
evals/cases.yaml
references/cross-cutting.md
references/testing-recipes.md
scripts/verify.sh

The instruction itself

12 sections, as written by the author

NestJS

Build server-side Node apps the way NestJS intends: feature modules, providers wired through the DI container, controllers, and a cross-cutting layer bound at a deliberate scope. This skill is about Nest-specific mechanics — how DI scopes resolve, what order the request lifecycle runs in, where to bind guards/pipes/interceptors/filters, and how to test it with Test.createTestingModule.

Not this — route instead

  • Bare Express/Fastify/http service, no @Module/@Injectable../nodejs/SKILL.md. Nest starts the moment the DI container appears.
  • REST resource modeling, versioning, status codes, idempotency (framework-agnostic) → ../api-design/SKILL.md. Nest is where you *implement* those decisions.
  • Designing the schema / writing queries / migrations → ../prisma-orm/SKILL.md. *Injecting* a repo or DataSource provider stays here; designing the table does not.
  • Generic JS/TS test infra (Jest config, coverage thresholds, monorepo) → ../testing-web/SKILL.md. The Nest harness (TestingModule, overrideProvider, Supertest bootstrap) stays here.

Mental model

Everything is a provider in a directed DI graph. Modules draw the boundaries of that graph — a provider is only reachable where it is provided or imported. Cross-cutting concerns (guards, interceptors, pipes, filters) are decorators bound at a scope you choose: global, controller, or route. Get those three right and the rest is plumbing.

The request lifecycle runs in a fixed order. Memorize it — most "my guard can't see the validated body" bugs are an ordering misunderstanding:

req → middleware → guards → interceptors(pre) → pipes → handler → interceptors(post) → exception filters → res

So pipes run *after* guards (a guard cannot read a transformed DTO), and filters catch everything thrown downstream. Source: NestJS request-lifecycle docs.

Module design

One feature module per bounded context. Rules, each with its why:

  • exports is the module's public API. A provider not exported is private to that module — that is the encapsulation, lean on it.
  • imports brings in another module's exports; it does not re-declare providers. Re-declaring a provider in two modules gives you two singletons and silent state bugs.
  • Keep AppModule thin. It wires feature modules and global config, nothing else. A god AppModule that declares every controller becomes an untestable circular-dependency magnet.
  • Use a dynamic module (forRoot / forRootAsync) for configurable infrastructure (DB, cache, mailer) so consumers pass options instead of editing the module.
// Bad — everything dumped in AppModule, no boundaries
@Module({ controllers: [OrdersController, UsersController, BillingController],
          providers: [OrdersService, UsersService, BillingService, PrismaService] })
export class AppModule {}

// Good — a feature module owns its slice and exports only its public surface
@Module({
  imports: [PrismaModule],
  controllers: [OrdersController],
  providers: [OrdersService],
  exports: [OrdersService], // other modules consume the service, not the repo
})
export class OrdersModule {}

Providers & DI

Pick the custom-provider form by intent:

| Form | Use it when | Resolved by |

|------|-------------|-------------|

| useClass | Default — swap implementation by class (e.g. real vs fake mailer) | Nest instantiates |

| useValue | A ready object/constant: config, a mock in tests | Used as-is |

| useFactory | Value needs computing or other providers (inject: [...]) | Your factory fn |

| useExisting | Alias an existing token to a new token | Reuses instance |

A non-class token needs explicit injection — Nest has no type to reflect on:

const STRIPE = 'STRIPE_CLIENT';

@Module({
  providers: [{
    provide: STRIPE,
    useFactory: (cfg: ConfigService) => new Stripe(cfg.get('STRIPE_KEY')),
    inject: [ConfigService],
  }],
  exports: [STRIPE],
})
export class PaymentsModule {}

@Injectable()
export class CheckoutService {
  constructor(@Inject(STRIPE) private readonly stripe: Stripe) {}
}

forwardRef(() => X) is a last resort, not a fix — it works around a circular dependency that usually signals two modules that should share a third. Reach for the refactor first; if you must, forwardRef goes on *both* sides. See the anti-patterns table.

Provider scopes

| Scope | Lifetime | Use when |

|-------|----------|----------|

| DEFAULT | Singleton (one per app) | Almost always — stateless services |

| REQUEST | New instance per request | You genuinely need per-request state (@Inject(REQUEST) for the live request) |

| TRANSIENT | New instance per consumer | Each injector gets its own copy |

REQUEST scope bubbles up: any provider that injects a request-scoped provider becomes request-scoped too, and so does the controller — with a real per-request instantiation cost. Default to singleton; reach for REQUEST only when you must.

@Injectable({ scope: Scope.REQUEST })
export class RequestContext {
  constructor(@Inject(REQUEST) private readonly req: Request) {}
  get userId() { return this.req.user?.id; }
}

The classic gotcha: a request-scoped provider injected into a guard reads as undefined or stale because guards run early and the scope propagation is not what you assumed. If a guard needs request data, pull it from ExecutionContext (context.switchToHttp().getRequest()), not from an injected request-scoped service.

Cross-cutting layer

Pick the primitive by what it is *for*:

| Primitive | Job | Signature |

|-----------|-----|-----------|

| Guard | Authorize — allow/deny the request | returns boolean / Promise<boolean> |

| Interceptor | Wrap the handler before and after (logging, transform, timeout, cache) | RxJS, handle().pipe(...) |

| Pipe | Validate and/or transform an input argument | returns transformed value or throws |

| Exception filter | Catch a thrown error and shape the response | catch(exception, host) |

Then pick the binding scope:

| Binding | Reach | Can inject deps? |

|---------|-------|------------------|

| APP_GUARD / APP_PIPE / APP_INTERCEPTOR / APP_FILTER token in a module's providers | Global | Yes — resolved by the DI container |

| @UseGuards(X) / @UsePipes(X) on controller or route | Local | Yes if you pass the class |

| app.useGlobalGuards(new X()) in main.ts | Global | No — you instantiated it yourself |

The gotcha that bites everyone: app.useGlobalPipes(new ValidationPipe()) works, but a guard or pipe that needs to inject a ConfigService cannot be registered with new — Nest never resolved it. Use the APP_* token instead so the container builds it:

// Good — global AND DI-capable
@Module({
  providers: [{ provide: APP_GUARD, useClass: AuthGuard }],
})
export class AppModule {}

Deeper material — ExecutionContext, custom param decorators, Reflector + SetMetadata for role/@Public() guards, transform/timeout interceptors, filter shape, multiple-binding order — is in references/cross-cutting.md.

Validation

DTO + ValidationPipe + class-validator/class-transformer. The production-safe config:

// main.ts
app.useGlobalPipes(new ValidationPipe({
  whitelist: true,            // strip properties with no decorator
  forbidNonWhitelisted: true, // 400 on unknown properties instead of silently dropping
  transform: true,            // coerce payloads to DTO class instances (and primitives)
}));
export class CreateOrderDto {
  @IsString() @IsNotEmpty()
  sku: string;

  @IsInt() @Min(1)
  quantity: number;
}

If the pipe needs to inject something, bind it globally via APP_PIPE instead of new (same DI rule as above).

Testing

Test.createTestingModule({...}).compile() returns a TestingModule you pull providers from. Decision line: unit-test a provider with its collaborators mocked; e2e-test the wired app over HTTP.

Unit — mock the collaborators:

const moduleRef = await Test.createTestingModule({
  providers: [OrdersService],
})
  .overrideProvider(OrderRepository)
  .useValue({ findById: vi.fn().mockResolvedValue(order) })
  .compile();

const service = moduleRef.get(OrdersService);

e2e — boot the real app and hit it with Supertest. Replicate the global config from main.ts (pipes, filters, guards) or the test passes while prod 400s:

const app = moduleRef.createNestApplication();
app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true })); // mirror main.ts
await app.init();
await request(app.getHttpServer()).post('/orders').send(body).expect(201);

overrideGuard(AuthGuard).useValue({ canActivate: () => true }) lets an e2e test bypass auth. Full recipes — mocking a repo, request-scoped via resolve(), ConfigModule in tests, Vitest vs Jest — in references/testing-recipes.md.

Bootstrap & tooling

  • Scaffold with the CLI, not by hand: nest g resource orders, nest g module orders, nest g service orders. It wires the module registration for you.
  • NestJS 11 is current (Jan 2025), requires Node.js 20+, and ships Express v5 as the default HTTP adapter (Fastify remains an option).
  • Use the SWC builder for dev — roughly 20x faster builds and faster cold start than tsc. Keep tsc for type-checking in CI.
  • ESM is first-class in v11 (top-level await aligned with modern Node).
  • The official harness is moving toward Vitest (SWC-powered) alongside Jest; Jest is still fully supported and is what nest new historically scaffolds.

Anti-patterns

| Anti-pattern | Why it hurts | Do instead |

|--------------|--------------|------------|

| God AppModule declaring every controller/provider | No boundaries; breeds circular deps; untestable | One feature module per bounded context, exports = public API |

| new OrdersService(repo) inside a controller/service | Bypasses DI; same class becomes two unmocked instances | Constructor-inject; let the container build it |

| Business logic in the controller | Controllers should map HTTP ↔ service calls only | Push logic into a provider; controller stays thin |

| Scope.REQUEST by default | Bubbles up the chain, per-request cost, surprise undefined in guards | Default singleton; REQUEST only with a real reason |

| useGlobalPipes(new X()) for a pipe that needs deps | new is not DI-resolved; injected deps are undefined | Bind via APP_PIPE / APP_GUARD token in providers |

| e2e test that skips main.ts globals | Green test, red prod — validation/filters not applied | Replicate global pipes/filters/guards in the e2e bootstrap |

| forwardRef sprinkled to silence "circular dependency" | Hides the real coupling; fragile bootstrap order | Refactor to a shared module; forwardRef only as last resort, on both sides |

Verify

Run scripts/verify.sh [dir] to statically catch DI bypasses without a Nest install. It hard-fails only on new XxxService(...) outside test files; everything else is a warning. Exits 0 on a clean or empty target.

How to use it

Copy the folder

Take ericrisco/nestjs from the repository into ~/.claude/skills for personal use, or into .claude/skills inside a project.

Check the name does not clash

The agent identifies a skill by the name field in its header. Two skills with the same name cannot sit side by side — one of them will be ignored.