目次を表示する

NestJS Deep Dive 2026 ─ 内部構造・再発明回避・パフォーマンスを 10 章で読み解く

NestJS が提供する 18 のモジュール

第5章: NestJS が提供する 18 のモジュール

18 モジュールマップ

第1部で内部構造を見た。第2部の最初の章として、本章は NestJS が提供する 18 のモジュール を網羅する。網羅すること自体に意味がある ─ 「自分が書いている処理は、NestJS の何かと被っていないか」を即座に判定する地図を持つ。

各モジュールについて、(a) 概要 / (b) 使い方の核 / (c) 落とし穴 / (d) 再発明あるある の 4 点を整理する。

18 モジュールの分類マップ

mindmap
  root((NestJS<br/>18 modules))
    キャッシュ・状態
      1 CacheModule
    スケジューリング
      2 ScheduleModule
    監視・運用
      3 TerminusModule
      12 Logger
    設定・セキュリティ
      4 ConfigModule
      5 ThrottlerModule
    イベント・キュー
      6 EventEmitter
      7 BullMQ
    通信・統合
      8 Microservices
      9 GraphQL
      10 WebSocket / SSE
    入出力処理
      11 ValidationPipe
      13 ExceptionFilter
    アーキテクチャ
      14 CQRS
    開発支援
      15 Swagger
      16 Testing
    Discovery
      17 DiscoveryService
      18 LazyModuleLoader

順に見ていく。

1. CacheModule(@nestjs/cache-manager)

概要:HTTP レスポンスやサービス層のキャッシュを宣言的に管理。v11 で cache-manager v6 + Keyv ベースに刷新された。

import { CacheModule } from '@nestjs/cache-manager';
import { createKeyv } from '@keyv/redis';

@Module({
  imports: [CacheModule.registerAsync({
    isGlobal: true,
    useFactory: () => ({
      stores: [createKeyv('redis://localhost:6379')],
      ttl: 30_000, // ms ← v6 から ミリ秒 に変更
    }),
  })],
})
export class AppModule {}

// 宣言的キャッシュ
@UseInterceptors(CacheInterceptor)
@CacheKey('cats_all')
@CacheTTL(60_000)
@Get()
findAll() { return this.svc.findAll(); }

落とし穴

  • TTL 単位が秒 → ミリ秒に変更(v5 → v6 で)。「60 秒のつもりが 60ms になる」事故が多発
  • Keyv は遅延 eviction、get() を呼ばない限り期限切れエントリがメモリに残る
  • CacheInterceptor は POST/PUT/DELETE をデフォルトで無視

再発明あるある

  • private cache = new Map<string, { value, expiresAt }>() を service に直書き
  • 独自 @Cacheable() decorator を Reflector で実装
  • クラスタ展開後に「Pod ごとにキャッシュが食い違う」問題で慌てて Redis に寄せる

2. ScheduleModule(@nestjs/schedule)

概要:cron / interval / timeout をデコレータで宣言。SchedulerRegistry で動的追加・削除。

@Injectable()
export class TasksService {
  @Cron(CronExpression.EVERY_30_SECONDS, { name: 'cleanup' })
  handleCron() { /* ... */ }

  @Interval(10_000)
  handleInterval() { /* ... */ }

  constructor(private registry: SchedulerRegistry) {}
}

落とし穴

  • 複数 Pod で同じジョブが多重実行される(distributed lock や leader election を別途必要)
  • 永続化機能なし:再起動で動的ジョブは消える

再発明あるある

  • node-cronbootstrap() で直接叩く → DI と乖離してテストできない
  • setInterval(() => svc.run(), 60_000)OnApplicationBootstrap で起動して終了処理を忘れる

3. TerminusModule(@nestjs/terminus)

概要/health エンドポイントの実装に必要な部品をフルセットで提供。HTTP / DB / Memory / Disk / Microservice の Indicator あり。

@Controller('health')
export class HealthController {
  constructor(
    private health: HealthCheckService,
    private db: TypeOrmHealthIndicator,
    private mem: MemoryHealthIndicator,
  ) {}

  @Get() @HealthCheck()
  check() {
    return this.health.check([
      () => this.db.pingCheck('db'),
      () => this.mem.checkHeap('memory_heap', 200 * 1024 * 1024),
    ]);
  }
}

落とし穴

  • 全 Indicator が ok でないと 503 Service Unavailable(K8s liveness と readiness の使い分け要注意)
  • MicroserviceHealthIndicator のタイムアウト設定必須

再発明あるある

  • @Get('/health') health() { return { status: 'ok' }; } という固定文字列返し
  • DB ping を自前で SELECT 1 書いて try-catch で 200/500 を出し分け
  • メモリチェックを process.memoryUsage() で自前判定

4. ConfigModule(@nestjs/config)

概要:dotenv + バリデーション + 名前空間付き型安全 config を一元管理。

ConfigModule.forRoot({
  isGlobal: true,
  envFilePath: ['.env.local', '.env'],
  validationSchema: Joi.object({
    NODE_ENV: Joi.string().valid('dev','prod','test').required(),
    PORT: Joi.number().port().default(3000),
    DATABASE_URL: Joi.string().uri().required(),
  }),
});

// 名前空間
export default registerAs('database', () => ({
  url: process.env.DATABASE_URL,
}));

// 型安全 inject
constructor(@Inject(databaseConfig.KEY) private cfg: ConfigType<typeof databaseConfig>) {}

落とし穴

  • forFeature()validationSchema をサポートしない(issue #96)。registerAs 内で Joi 検証を呼ぶ必要
  • ConfigService.get<string>('FOO', { infer: true }) を付けないと型が unknown

再発明あるある

  • dotenv.config()main.ts で直書き、process.env.X を service で散らかす
  • 自前 Configuration クラスを new して static で保持

5. ThrottlerModule(@nestjs/throttler)

概要:Rate limiting の公式実装。v5 以降は複数 throttler 名(short/medium/long)を同時定義

ThrottlerModule.forRoot([
  { name: 'short', ttl: 1000, limit: 3 },
  { name: 'long',  ttl: 60000, limit: 100 },
]);

@SkipThrottle({ short: true })
@Throttle({ long: { limit: 5, ttl: 60000 } })
@Get() findAll() {}

落とし穴

  • 公式に Redis storage は同梱されない@nest-lab/throttler-storage-redis 等のコミュニティ実装を使う
  • 単純なメモリストアは複数 Pod で破綻
  • getTracker() をオーバーライドしないとリバースプロキシ配下で全 IP が同一になりがち

再発明あるある

  • 自前で Redis INCR + EXPIRE を Guard に書く
  • Map<ip, count> を service に持たせるだけの「単機運用前提」rate limiter

6. EventEmitterModule(@nestjs/event-emitter)

概要:プロセス内 pub/sub。内部で EventEmitter2、wildcard・namespace・非同期ハンドラに対応。

EventEmitterModule.forRoot({ wildcard: true, delimiter: '.' });

// publisher
this.emitter.emit('order.created', new OrderCreated(order));

// subscriber
@OnEvent('order.*') handleAny(payload: any) {}
@OnEvent('order.created', { async: true }) handleCreated(payload: OrderCreated) {}

落とし穴

  • プロセス内のみ。マイクロサービス間の通知に流用するとサイレント失敗
  • イベント名の typo を防ぐ仕組みは標準で無い

再発明あるある

  • 自前 Subject<Event> を service の static field に持つ「シングルトン event bus」

7. BullMQ 連携(@nestjs/bullmq)

概要:Redis-backed の job queue。@nestjs/bull(旧 Bull)は maintenance mode、新規は BullMQ 推奨。

BullModule.forRoot({ connection: { host: 'localhost', port: 6379 } });
BullModule.registerQueue({ name: 'audio' });

@Processor('audio')
export class AudioProcessor extends WorkerHost {
  async process(job: Job<{ file: string }>) {
    return await this.transcoder.run(job.data.file);
  }
  @OnWorkerEvent('completed') onCompleted(job: Job) {}
}

// producer
await this.queue.add('transcode', { file: 'a.mp3' }, { attempts: 3, backoff: 5000 });

落とし穴

  • BullMQ は Worker と Queue を別 connection で動かす設計。共有時 Redis の maxRetriesPerRequest: null 設定が必要
  • @OnWorkerEvent(worker ローカル)と @OnQueueEvent(グローバル via Redis)の取り違え
  • removeOnComplete を指定しないと完了 job が Redis に積もる

再発明あるある

  • Queue<Job>Array で持って setTimeout で順番に消化
  • 「失敗時 3 回までリトライ」を try/catch + sleep ループで自前実装
  • DLQ(Dead Letter Queue)を別 collection 手書き

8. Microservices(@nestjs/microservices)

概要:transport 抽象化されたサービス間通信。TCP / Redis / NATS / RabbitMQ / Kafka / gRPC / MQTT を同じ API で扱える。

// server
const app = await NestFactory.createMicroservice<MicroserviceOptions>(AppModule, {
  transport: Transport.RMQ,
  options: { urls: ['amqp://localhost'], queue: 'orders_queue' },
});

@Controller()
export class OrderHandler {
  @MessagePattern('order.create') create(d: CreateDto) { return this.svc.create(d); }
  @EventPattern('user.signed_up') sideEffect(p: any) {}
}

落とし穴

  • MessagePattern(req/res)と EventPattern(fire-and-forget)の混同
  • gRPC は .proto ロード設定がデリケート
  • RabbitMQ で noAck: false のとき例外で stuck

再発明あるある

  • amqplib を service に直に new して reconnect ロジックを毎回書く
  • kafkajs を raw で使い、partitioning とリトライを毎プロジェクト書き直す

9. GraphQL(@nestjs/graphql)

概要:Code-First / Schema-First 両対応、Apollo / Mercurius どちらのドライバも選択可。

GraphQLModule.forRoot<ApolloDriverConfig>({
  driver: ApolloDriver,
  autoSchemaFile: 'schema.gql',
  context: ({ req }) => ({ req, loaders: createLoaders() }),
});

@Resolver(() => User)
class UserResolver {
  @Query(() => [User]) users() {}
  @ResolveField(() => [Post]) posts(@Parent() user: User, @Context() ctx: any) {
    return ctx.loaders.postsByUser.load(user.id);
  }
}

落とし穴

  • DataLoader は公式モジュールに含まれないnestjs-dataloader(krislefeber)等を使うか自前で context factory に詰める
  • N+1 対策をしないと resolver が再帰的に DB を叩く
  • subscriptions は Apollo Driver で graphql-ws 推奨(subscriptions-transport-ws deprecated)

再発明あるある

  • DataLoader 風のバッチローダを Map と Promise.all で自前実装

10. WebSocket / SSE

概要:WebSocket は @WebSocketGateway で Socket.io / native ws を抽象化。SSE は @Sse() decorator で Observable<MessageEvent> を返すだけ。

// WebSocket
@WebSocketGateway(3001, { namespace: 'chat', cors: true })
export class ChatGateway {
  @WebSocketServer() server: Server;
  @SubscribeMessage('message')
  onMsg(@MessageBody() data: any, @ConnectedSocket() c: Socket) {
    this.server.to('room1').emit('message', data);
  }
}

// SSE
@Sse('events')
events(): Observable<MessageEvent> {
  return interval(1000).pipe(map(i => ({ data: { tick: i } })));
}

落とし穴

  • SSE は reverse proxy のバッファリング(nginx の X-Accel-Buffering: no)に依存
  • WebSocket gateway はバージョン依存が強い

再発明あるある

  • socket.iomain.ts で直接 attach して DI と無関係に作る
  • SSE を Express middleware で res.write('data: ...') 直書き

11. Validation / Transform Pipes

概要ValidationPipeclass-validator + class-transformer で DTO を自動検証。

app.useGlobalPipes(new ValidationPipe({
  whitelist: true, forbidNonWhitelisted: true,
  transform: true, transformOptions: { enableImplicitConversion: true },
}));

class CreateUserDto {
  @IsEmail() email!: string;
  @IsInt() @Min(18) age!: number;
}

落とし穴

  • class-validator / class-transformer は更新が滞っている。NestJS 公式も置き換え検討中(issue #1735, #8390)
  • ネストオブジェクトには @ValidateNested() @Type(() => Sub) の二点セットが必須
  • typia 比 15,000倍遅い(第8章で詳述)

再発明あるある

  • 自前の if (!body.email) throw new BadRequestException() 詰め込み
  • Joi を service の入口で実行して NestJS の Pipe システムをバイパス
  • Zod を pipe.transform() 内で書き直す(実は ZodValidationPipe 相当が nestjs-zod にある)

12. Logger(公式)

概要Logger クラス + LoggerService インターフェース。app.useLogger() で差し替え可能。

// 標準
private readonly logger = new Logger(UserService.name);
this.logger.log({ userId, event: 'created' }, 'User created');

// nestjs-pino
app.useLogger(app.get(Logger));
imports: [LoggerModule.forRoot({ pinoHttp: { level: 'info', autoLogging: true } })];

落とし穴

  • ビルトイン Logger は同期 stdout 書き込み。ホットパスをブロック
  • 構造化ログは Pino / Winston との統合が定石

再発明あるある

  • console.log() 散在
  • class MyLogger { info(){} warn(){} } を自作して app.useLogger を忘れる
  • Request ID 生成と log 結合を自前で middleware に書く(CLS でやれば 1 行)

13. Exception Filters

概要@Catch() decorator + ExceptionFilter インターフェース。HttpException 系を投げれば標準で正しい HTTP ステータスが返る。

@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
  catch(ex: unknown, host: ArgumentsHost) {
    const ctx = host.switchToHttp(); const res = ctx.getResponse<Response>();
    const status = ex instanceof HttpException ? ex.getStatus() : 500;
    res.status(status).json({ statusCode: status, message: (ex as Error).message });
  }
}

// global registration via APP_FILTER
{ provide: APP_FILTER, useClass: AllExceptionsFilter }

落とし穴

  • useGlobalFilters(new Filter()) だと filter 内で他 provider を inject できない(APP_FILTER 経由必須
  • request-scoped global filter はビルトイン例外を catch しない既知 issue (#6429)

再発明あるある

  • 全 Service メソッドに try/catch を書いて HttpException を throw
  • 独自 Result<T, E> 型で例外を握りつぶし、Controller 側で if 分岐

14. CQRS Module(@nestjs/cqrs)

概要:Command / Query / Event を分離する DDD 系の枠組み。Saga は RxJS で event stream をフィルタする。

class CreateUserCommand { constructor(public dto: CreateUserDto) {} }

@CommandHandler(CreateUserCommand)
class CreateUserHandler implements ICommandHandler<CreateUserCommand> {
  async execute(cmd: CreateUserCommand) { /* ... */ }
}

@Injectable()
class UserSagas {
  @Saga()
  signedUp = (events$: Observable<any>) =>
    events$.pipe(ofType(UserCreatedEvent), map(() => new SendWelcomeEmail()));
}

落とし穴

  • 1 コマンドに対し 1 ハンドラ(複数登録すると最後勝ち)
  • 過剰適用しがち(CRUD まで Command/Query 分離して保守コスト増)

再発明あるある

  • 「サービス層に書いた write/read のメソッド」を分けず混在
  • 自前 DomainEventBus を Subject ベースで作って DI から外れる

15. Swagger(@nestjs/swagger)

概要:OpenAPI v3 スキーマを decorator から自動生成。

@ApiTags('users')
@Controller('users')
class UsersController {
  @ApiOperation({ summary: '新規ユーザー作成' })
  @ApiResponse({ status: 201, type: UserResponseDto })
  @Post() create(@Body() dto: CreateUserDto) {}
}

自動生成の限界

  • ジェネリック型(Page<T>)は型情報がランタイムに残らない、明示的にラップ DTO を作る
  • CLI plugin を有効にしないと @ApiProperty を全フィールドに書く必要

再発明あるある

  • OpenAPI yaml を別途手書きしてコミットし、コードと乖離していく

16. Testing utilities(@nestjs/testing)

概要Test.createTestingModule() で実プロダクションと同じ DI ツリーを構築し、overrideProvider 等でモックに差し替え。

const moduleRef = await Test.createTestingModule({ imports: [AppModule] })
  .overrideProvider(MailService).useValue({ send: jest.fn() })
  .overrideGuard(AuthGuard).useValue({ canActivate: () => true })
  .compile();
const app = moduleRef.createNestApplication();
await app.init();

落とし穴

  • request-scoped provider は moduleRef.resolve() で都度取り出す(get() だと例外)

再発明あるある

  • DI コンテナを丸ごと jest.mock() で置換(NestJS の意図と逆)
  • 各テストで new Service(new RepoMock()) を手書きして本番 DI ツリーと乖離

17. DiscoveryService(@nestjs/core)

概要:第2章で見た「特定 Decorator が付いた全 method を集める」自前フレームワーク的拡張のための API。

@Injectable()
export class EventListenerLoader implements OnModuleInit {
  constructor(
    private discovery: DiscoveryService,
    private scanner: MetadataScanner,
  ) {}

  onModuleInit() {
    const providers = this.discovery.getProviders();
    for (const wrapper of providers) {
      const { instance } = wrapper;
      if (!instance) continue;
      const proto = Object.getPrototypeOf(instance);
      this.scanner.getAllMethodNames(proto).forEach(method => {
        const eventName = Reflect.getMetadata('event_listener', proto[method]);
        if (eventName) this.register(eventName, instance, method);
      });
    }
  }
}

@OnEvent / @Cron / @CommandHandler の内部で実際に行われていること。社内独自の Annotation 駆動 Framework を作るときの基盤。

18. LazyModuleLoader

第4章で扱った Lazy Module の Loader API。

const moduleRef = await this.lazyModuleLoader.load(() =>
  import('./tasks/heavy-task.module').then(m => m.HeavyTaskModule)
);
const service = moduleRef.get(HeavyTaskService);

Controller / Resolver / Gateway / Middleware / Global Module / Global enhancer は lazy load 不可。第4章を参照。

18 モジュール 早見表 ─ 「自前で書いていないか」

カテゴリNestJS 公式自前で書きがちな代替
キャッシュCacheModuleMap + setInterval cleanup
スケジューラScheduleModulenode-cron / setInterval 直書き
ヘルスチェックTerminusModule固定文字列 /health
設定ConfigModuledotenv 直叩き
Rate LimitThrottlerModule自前 Map / Redis INCR
イベントEventEmitterModule自前 Subject bus
キュー@nestjs/bullmqin-memory + setTimeout
マイクロサービス@nestjs/microservicesamqplib / kafkajs 直書き
GraphQL@nestjs/graphqlApollo 直接設定
WebSocket / SSEGateway / @Sse()socket.io 直 attach、SSE 手書き
バリデーションValidationPipe自前 if + throw
ロガーLogger / nestjs-pinoconsole.log 散在
例外ExceptionFilterService 内 try-catch
CQRS@nestjs/cqrs自前 EventBus
OpenAPI@nestjs/swaggeryaml 手書きコミット
テスト@nestjs/testingjest.mock で全部置換
DiscoveryDiscoveryServiceリフレクション直書き
Lazy LoadLazyModuleLoaderdynamic import 直接

本章の要点

#要点
1NestJS は 18 のモジュールを提供。自分が書いてる処理がどれかと被ってないか棚卸しする
2CacheModule は v11 で TTL 単位がミリ秒に変更、cache-manager v6 + Keyv ベース
3ScheduleModule は 複数 Pod で多重実行、distributed lock 別途必要
4ThrottlerModule は Redis storage が公式同梱されない、コミュニティ実装を使う
5BullMQ は @nestjs/bull の後継、Worker と Queue は別 connection
6GraphQL は DataLoader が公式に含まれない、別途必要
7ValidationPipe の class-validator / class-transformer は更新滞り中、Zod / typia への移行検討
8Logger は同期 stdout 書き込みでホットパスをブロック、Pino へ移行が定石
9DiscoveryService + MetadataScanner で社内独自の Annotation 駆動 Framework が書ける

効いている根本原理

本章は 原理4(メモリ効率は DI スコープ × ストリーム × Logger 設計に集約される) の前段として、「公式が提供しているもの」を地図化した。次章では、ここで触れた標準モジュールでもカバーされていない領域 ─ pagination、retry、circuit breaker、distributed lock、tenant context、idempotency 等 ─ を準公式パッケージで埋める方法を見ていく。