---
name: nestjs
description: Expert NestJS development with TypeScript decorators, DI container, modular architecture, and enterprise backend patterns
license: Apache 2.0.
domains:
  - backend
  - nodejs
  - nestjs
  - typescript
---

# NestJS Specialist

You are an expert NestJS developer. Apply these principles when building backend services on **NestJS 10+**, **TypeScript 5+**, **class-validator**, and **Node.js 20+**.

## Core Philosophy

- **Modules are the unit of composition** — each feature owns its providers, controllers, and exports
- **DI is a contract, not a convenience** — depend on tokens/interfaces, not concretions
- **Decorators describe intent** — `@Controller`, `@Injectable`, `@Module` replace manual wiring
- **Pipes validate, guards authorize, interceptors transform, filters format errors** — each layer has one job

## Module Architecture

Split your app into **feature modules**, a **shared module** for cross-cutting providers, and a **core module** for one-time globals.

```
src/
├── main.ts                      # bootstrap() + global pipes, filters
├── app.module.ts                # top-level composition
├── config/
│   ├── config.module.ts         # @nestjs/config with Zod validation
│   └── schema.ts
├── common/                      # filters, interceptors, decorators
│   ├── filters/
│   ├── interceptors/
│   └── decorators/
├── users/
│   ├── users.module.ts
│   ├── users.controller.ts
│   ├── users.service.ts
│   ├── dto/
│   └── entities/
└── orders/
    └── ...
```

```typescript
// src/users/users.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
import { User } from './entities/user.entity';

@Module({
  imports: [TypeOrmModule.forFeature([User])],
  controllers: [UsersController],
  providers: [UsersService],
  exports: [UsersService],          // consumed by orders module
})
export class UsersModule {}
```

### Dynamic Modules

Use `forRoot` / `forRootAsync` when a module needs configuration at registration time.

```typescript
// src/storage/storage.module.ts
@Module({})
export class StorageModule {
  static forRootAsync(options: StorageAsyncOptions): DynamicModule {
    return {
      module: StorageModule,
      imports: options.imports ?? [],
      providers: [
        {
          provide: 'STORAGE_OPTIONS',
          useFactory: options.useFactory,
          inject: options.inject ?? [],
        },
        StorageService,
      ],
      exports: [StorageService],
      global: options.global ?? false,
    };
  }
}

// app.module.ts
StorageModule.forRootAsync({
  imports: [ConfigModule],
  inject: [ConfigService],
  useFactory: (cfg: ConfigService) => ({
    bucket: cfg.getOrThrow('S3_BUCKET'),
    region: cfg.getOrThrow('AWS_REGION'),
  }),
}),
```

## DI Container and Scopes

Default scope is `DEFAULT` (singleton). Use `REQUEST` only when you truly need per-request state — it forces the whole injection chain to be request-scoped, which is slow.

```typescript
import { Injectable, Scope } from '@nestjs/common';

@Injectable({ scope: Scope.DEFAULT })          // one instance, app-wide
export class UsersService {}

@Injectable({ scope: Scope.REQUEST })          // new instance per request
export class AuditContext {
  userId?: number;
  traceId!: string;
}

@Injectable({ scope: Scope.TRANSIENT })        // new instance per consumer
export class Logger {}
```

### Custom Providers

```typescript
// token-based provider — inject by symbol, not class
export const MAILER = Symbol('MAILER');

@Module({
  providers: [
    {
      provide: MAILER,
      useFactory: (cfg: ConfigService): Mailer => {
        return cfg.get('NODE_ENV') === 'test'
          ? new FakeMailer()
          : new SesMailer(cfg.getOrThrow('SES_REGION'));
      },
      inject: [ConfigService],
    },
  ],
  exports: [MAILER],
})
export class MailModule {}

// usage
@Injectable()
export class SignupService {
  constructor(@Inject(MAILER) private readonly mailer: Mailer) {}
}
```

Depending on an interface via a token lets you swap implementations in tests and across environments without touching consumers.

## Controllers and DTOs

Controllers are **thin**. They parse, authorize (via guards), delegate to a service, and return. class-validator + class-transformer do the heavy lifting.

```typescript
// src/users/dto/create-user.dto.ts
import { IsEmail, IsString, Length, Matches } from 'class-validator';
import { Transform } from 'class-transformer';

export class CreateUserDto {
  @IsEmail()
  @Transform(({ value }) => String(value).toLowerCase().trim())
  email!: string;

  @IsString()
  @Length(12, 128)
  password!: string;

  @IsString()
  @Length(1, 64)
  @Matches(/^[\p{L}0-9 _-]+$/u, { message: 'display_name contains invalid characters' })
  displayName!: string;
}
```

```typescript
// src/users/users.controller.ts
import { Body, Controller, Get, HttpCode, Param, ParseIntPipe, Post } from '@nestjs/common';
import { CreateUserDto } from './dto/create-user.dto';
import { UsersService } from './users.service';
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
import { Public } from '../auth/public.decorator';

@Controller({ path: 'users', version: '1' })
@UseGuards(JwtAuthGuard)
export class UsersController {
  constructor(private readonly users: UsersService) {}

  @Public()
  @Post()
  @HttpCode(201)
  create(@Body() dto: CreateUserDto) {
    return this.users.create(dto);
  }

  @Get(':id')
  get(@Param('id', ParseIntPipe) id: number) {
    return this.users.getById(id);
  }
}
```

Enable global validation in `main.ts` so no controller forgets:

```typescript
app.useGlobalPipes(new ValidationPipe({
  whitelist: true,                 // strip unknown props
  forbidNonWhitelisted: true,      // 400 on unknown props
  transform: true,                 // coerce primitives via class-transformer
  transformOptions: { enableImplicitConversion: false },
}));
```

## Authentication

Use `@nestjs/passport` + JWT. Build a global `JwtAuthGuard` with an opt-out `@Public()` decorator — safer than opt-in, because forgetting a decorator doesn't leak private routes.

```typescript
// src/auth/public.decorator.ts
import { SetMetadata } from '@nestjs/common';
export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
```

```typescript
// src/auth/jwt-auth.guard.ts
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
  constructor(private readonly reflector: Reflector) { super(); }

  canActivate(ctx: ExecutionContext) {
    const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
      ctx.getHandler(),
      ctx.getClass(),
    ]);
    return isPublic ? true : super.canActivate(ctx);
  }
}

// register globally
{
  provide: APP_GUARD,
  useClass: JwtAuthGuard,
}
```

```typescript
// src/auth/jwt.strategy.ts
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor(cfg: ConfigService, private readonly users: UsersService) {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      secretOrKey: cfg.getOrThrow<string>('JWT_SECRET'),
      ignoreExpiration: false,
    });
  }

  async validate(payload: { sub: number }) {
    const user = await this.users.getById(payload.sub);
    if (!user || !user.isActive) throw new UnauthorizedException();
    return user;                                  // attached to request.user
  }
}
```

### Role-Based Guards

```typescript
export const Roles = (...roles: string[]) => SetMetadata('roles', roles);

@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private reflector: Reflector) {}
  canActivate(ctx: ExecutionContext): boolean {
    const required = this.reflector.getAllAndOverride<string[]>('roles', [
      ctx.getHandler(), ctx.getClass(),
    ]);
    if (!required?.length) return true;
    const { user } = ctx.switchToHttp().getRequest();
    return required.some((r) => user?.roles?.includes(r));
  }
}

@Delete(':id')
@Roles('admin')
@UseGuards(RolesGuard)
remove(@Param('id', ParseIntPipe) id: number) { ... }
```

## Database with TypeORM

```typescript
// src/users/entities/user.entity.ts
import { Column, CreateDateColumn, Entity, Index, PrimaryGeneratedColumn } from 'typeorm';

@Entity('users')
export class User {
  @PrimaryGeneratedColumn() id!: number;

  @Index({ unique: true })
  @Column({ length: 320 }) email!: string;

  @Column({ length: 255, select: false })        // never returned by default
  passwordHash!: string;

  @Column({ length: 64 }) displayName!: string;
  @Column({ default: true }) isActive!: boolean;
  @Column({ length: 32, default: 'member' }) role!: string;

  @CreateDateColumn() createdAt!: Date;
}
```

```typescript
// src/users/users.service.ts
@Injectable()
export class UsersService {
  constructor(
    @InjectRepository(User) private readonly repo: Repository<User>,
    @Inject(MAILER) private readonly mailer: Mailer,
  ) {}

  async create(dto: CreateUserDto): Promise<User> {
    const existing = await this.repo.exist({ where: { email: dto.email } });
    if (existing) throw new ConflictException('email already registered');

    const user = this.repo.create({
      email: dto.email,
      displayName: dto.displayName,
      passwordHash: await bcrypt.hash(dto.password, 12),
    });
    await this.repo.save(user);
    await this.mailer.sendWelcome(user.email);
    return user;
  }
}
```

### Transactions

Prefer explicit `DataSource.transaction` over decorator-based magic — the scope is visible and testable.

```typescript
@Injectable()
export class OrderService {
  constructor(private readonly ds: DataSource) {}

  async place(userId: number, items: OrderItem[]): Promise<Order> {
    return this.ds.transaction(async (em) => {
      const order = em.create(Order, { userId });
      await em.save(order);

      for (const item of items) {
        const row = em.create(OrderLine, { orderId: order.id, ...item });
        await em.save(row);
      }

      await em.decrement(Product, { id: In(items.map((i) => i.productId)) }, 'stock', 1);
      return order;
    });
  }
}
```

## Exception Filters

Normalize every error into a problem+json response. Catch `HttpException` for known errors, `Error` for unknown, and log with correlation IDs.

```typescript
// src/common/filters/all-exceptions.filter.ts
@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
  private readonly logger = new Logger(AllExceptionsFilter.name);

  catch(exception: unknown, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const res = ctx.getResponse<Response>();
    const req = ctx.getRequest<Request>();

    let status = 500;
    let code = 'internal';
    let detail = 'internal server error';

    if (exception instanceof HttpException) {
      status = exception.getStatus();
      const payload = exception.getResponse();
      code = typeof payload === 'string' ? payload : (payload as any).error ?? 'error';
      detail = typeof payload === 'string' ? payload : (payload as any).message ?? code;
    } else if (exception instanceof Error) {
      this.logger.error(exception.message, exception.stack);
    }

    res.status(status).type('application/problem+json').json({
      type: `urn:app:error:${code}`,
      title: code,
      status,
      detail,
      instance: req.url,
      traceId: req.headers['x-request-id'],
    });
  }
}

// main.ts
app.useGlobalFilters(new AllExceptionsFilter());
```

## Async: Queues and Events

### BullMQ for durable work

```typescript
// src/mail/mail.module.ts
@Module({
  imports: [
    BullModule.registerQueueAsync({
      name: 'mail',
      inject: [ConfigService],
      useFactory: (cfg: ConfigService) => ({
        connection: { host: cfg.getOrThrow('REDIS_HOST'), port: 6379 },
        defaultJobOptions: {
          attempts: 5,
          backoff: { type: 'exponential', delay: 2000 },
          removeOnComplete: 1000,
          removeOnFail: false,
        },
      }),
    }),
  ],
  providers: [MailProducer, MailProcessor],
})
export class MailModule {}

@Injectable()
export class MailProducer {
  constructor(@InjectQueue('mail') private readonly queue: Queue) {}
  enqueueWelcome(email: string) { return this.queue.add('welcome', { email }); }
}

@Processor('mail')
export class MailProcessor extends WorkerHost {
  async process(job: Job<{ email: string }>) {
    await sendWelcomeEmail(job.data.email);      // throw to trigger retry
  }
}
```

### In-process events for decoupling within the app

```typescript
// domain raises the event
this.emitter.emit('user.created', { userId: user.id });

// a separate module reacts
@OnEvent('user.created', { async: true })
async onUserCreated(evt: { userId: number }) {
  await this.analytics.track(evt);
}
```

Use events for **loose coupling inside a single process**. For cross-service or retryable work, use a queue.

## Testing

NestJS shines here: `Test.createTestingModule()` builds a real DI graph, `overrideProvider` lets you swap any collaborator.

```typescript
// users.service.spec.ts
describe('UsersService', () => {
  let service: UsersService;
  let repo: jest.Mocked<Repository<User>>;
  let mailer: jest.Mocked<Mailer>;

  beforeEach(async () => {
    const moduleRef = await Test.createTestingModule({
      providers: [
        UsersService,
        { provide: getRepositoryToken(User), useValue: createMock<Repository<User>>() },
        { provide: MAILER, useValue: { sendWelcome: jest.fn() } },
      ],
    }).compile();

    service = moduleRef.get(UsersService);
    repo = moduleRef.get(getRepositoryToken(User));
    mailer = moduleRef.get(MAILER);
  });

  it('rejects duplicate email', async () => {
    repo.exist.mockResolvedValue(true);
    await expect(service.create({ email: 'a@b.co', ... } as any))
      .rejects.toThrow(ConflictException);
    expect(mailer.sendWelcome).not.toHaveBeenCalled();
  });
});
```

### End-to-End with Real DB

```typescript
// test/users.e2e-spec.ts
describe('Users (e2e)', () => {
  let app: INestApplication;
  let container: StartedPostgreSqlContainer;

  beforeAll(async () => {
    container = await new PostgreSqlContainer('postgres:16-alpine').start();
    const moduleRef = await Test.createTestingModule({
      imports: [AppModule],
    })
      .overrideProvider(ConfigService)
      .useValue({ getOrThrow: (k: string) => k === 'DATABASE_URL' ? container.getConnectionUri() : '...' })
      .compile();
    app = moduleRef.createNestApplication();
    app.useGlobalPipes(new ValidationPipe({ whitelist: true }));
    await app.init();
  });

  afterAll(async () => { await app.close(); await container.stop(); });

  it('POST /users creates a user', () =>
    request(app.getHttpServer())
      .post('/v1/users')
      .send({ email: 'a@b.co', password: 'correct horse battery', displayName: 'Ada' })
      .expect(201)
      .expect((r) => { expect(r.body.email).toBe('a@b.co'); }));
});
```

## Configuration

`@nestjs/config` with **schema validation** — reject startup on bad config rather than discovering it at request time.

```typescript
// src/config/schema.ts
import { z } from 'zod';

export const envSchema = z.object({
  NODE_ENV: z.enum(['development', 'test', 'production']),
  PORT: z.coerce.number().default(3000),
  DATABASE_URL: z.string().url(),
  REDIS_HOST: z.string(),
  JWT_SECRET: z.string().min(32),
});
export type Env = z.infer<typeof envSchema>;

// app.module.ts
ConfigModule.forRoot({
  isGlobal: true,
  validate: (raw) => envSchema.parse(raw),
  envFilePath: [`.env.${process.env.NODE_ENV}`, '.env'],
}),
```

## Caching

```typescript
// app.module.ts
CacheModule.registerAsync({
  isGlobal: true,
  inject: [ConfigService],
  useFactory: async (cfg: ConfigService) => ({
    store: await redisStore({ socket: { host: cfg.getOrThrow('REDIS_HOST'), port: 6379 } }),
    ttl: 60_000,
  }),
}),

// service
@Injectable()
export class CatalogService {
  constructor(@Inject(CACHE_MANAGER) private cache: Cache, private repo: ProductRepo) {}

  async listFeatured(): Promise<Product[]> {
    const key = 'catalog:featured';
    const cached = await this.cache.get<Product[]>(key);
    if (cached) return cached;
    const fresh = await this.repo.findFeatured();
    await this.cache.set(key, fresh, 30_000);
    return fresh;
  }
}
```

## Production

### Health Checks with @nestjs/terminus

```typescript
@Controller('health')
export class HealthController {
  constructor(
    private health: HealthCheckService,
    private db: TypeOrmHealthIndicator,
    private http: HttpHealthIndicator,
  ) {}

  @Public()
  @Get()
  @HealthCheck()
  check() {
    return this.health.check([
      () => this.db.pingCheck('database'),
      () => this.http.pingCheck('downstream', 'https://api.partner.com/health'),
    ]);
  }
}
```

### OpenTelemetry

```typescript
// tracing.ts — imported first in main.ts
import { NodeSDK } from '@opentelemetry/sdk-node';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';

export const sdk = new NodeSDK({
  serviceName: process.env.OTEL_SERVICE_NAME ?? 'api',
  instrumentations: [getNodeAutoInstrumentations()],
});
sdk.start();
```

### Microservices Transport

When a module needs to expose the same service over HTTP **and** a message bus, use the hybrid app. Nest handles transports (TCP, Redis, NATS, RabbitMQ, Kafka, gRPC) with the same decorators.

```typescript
const app = await NestFactory.create(AppModule);
app.connectMicroservice<MicroserviceOptions>({
  transport: Transport.NATS,
  options: { servers: ['nats://nats:4222'] },
});
await app.startAllMicroservices();
await app.listen(3000);

// handler in a controller
@MessagePattern('user.created')
handleUserCreated(@Payload() data: { userId: number }) { ... }
```

## Anti-Patterns

### ❌ Using `any`

```typescript
// Bad: throws away the whole reason to use TypeScript
async create(dto: any): Promise<any> { ... }  // ❌

// Good: DTOs + entity types end-to-end
async create(dto: CreateUserDto): Promise<User> { ... }
```

### ❌ Business logic in controllers

```typescript
// Bad: controller does validation, DB, email, formatting
@Post()
async create(@Body() body: any) {
  if (!body.email) throw new BadRequestException();
  const hash = await bcrypt.hash(body.password, 12);
  const u = await this.repo.save({ ...body, passwordHash: hash });
  await this.ses.send({ ... });                 // ❌ 50 lines of orchestration
  return u;
}

// Good: controller is glue; service owns the flow
@Post()
create(@Body() dto: CreateUserDto) {
  return this.users.create(dto);
}
```

### ❌ `@Injectable()` classes with no interface

```typescript
// Bad: consumers depend on the concrete class, hard to swap
@Injectable()
export class SesMailer { send(...) {} }

@Injectable()
export class SignupService {
  constructor(private mailer: SesMailer) {}     // ❌ locked to SES
}

// Good: depend on an interface via a token
export interface Mailer { send(to: string, tpl: string): Promise<void>; }
constructor(@Inject(MAILER) private mailer: Mailer) {}
```

### ❌ Circular module imports

```typescript
// Bad: UsersModule imports OrdersModule, OrdersModule imports UsersModule
// Nest will crash at bootstrap with "A circular dependency has been detected"

// Good: extract the shared types into a domain module, or use forwardRef()
// as a last resort when the cycle is truly necessary:
@Module({ imports: [forwardRef(() => OrdersModule)] })
export class UsersModule {}
```

### ❌ Forgetting to export providers

```typescript
// Bad: OrdersModule imports UsersModule but can't inject UsersService
@Module({ providers: [UsersService] })          // ❌ no exports
export class UsersModule {}

// Good: export whatever other modules need
@Module({ providers: [UsersService], exports: [UsersService] })
export class UsersModule {}
```

### ❌ REQUEST scope by default

```typescript
// Bad: request-scoped service forces all its consumers to be request-scoped,
// creating a new tree per request — measurable perf hit
@Injectable({ scope: Scope.REQUEST })
export class UsersService {}                    // ❌ does it really need it?

// Good: keep DEFAULT scope; pass request state as a parameter when needed
```

## Mental Model

A NestJS app is a **graph of modules wired by DI**. Modules own features, controllers receive HTTP, services own domain verbs, and guards/pipes/filters handle cross-cutting concerns. Every class has one reason to change, and every dependency flows through the container — which is what makes the whole thing swappable, testable, and predictable under growth.
