# Nestjs

> 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`).

- Skill: `ericrisco/nestjs` (Agent Skill, multi-file: 6 files)
- Install (CLI): `npx skillmds@latest add ericrisco/nestjs`
- Raw SKILL.md: https://api.skillmd.com/api/skills/ericrisco/nestjs/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Integrations & APIs
- Author: ericrisco (https://skillmd.com/u/ericrisco)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/ericrisco/nestjs

---


# 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:

```text
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.

```typescript
// 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:

```typescript
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.

```typescript
@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:

```typescript
// 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:

```typescript
// 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)
}));
```

```typescript
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:

```typescript
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:

```typescript
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.

