第5章: NestJS が提供する 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-cronをbootstrap()で直接叩く → 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-wsdeprecated)
再発明あるある:
- 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.ioをmain.tsで直接 attach して DI と無関係に作る- SSE を Express middleware で
res.write('data: ...')直書き
11. Validation / Transform Pipes
概要:ValidationPipe が class-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 公式 | 自前で書きがちな代替 |
|---|---|---|
| キャッシュ | CacheModule | Map + setInterval cleanup |
| スケジューラ | ScheduleModule | node-cron / setInterval 直書き |
| ヘルスチェック | TerminusModule | 固定文字列 /health |
| 設定 | ConfigModule | dotenv 直叩き |
| Rate Limit | ThrottlerModule | 自前 Map / Redis INCR |
| イベント | EventEmitterModule | 自前 Subject bus |
| キュー | @nestjs/bullmq | in-memory + setTimeout |
| マイクロサービス | @nestjs/microservices | amqplib / kafkajs 直書き |
| GraphQL | @nestjs/graphql | Apollo 直接設定 |
| WebSocket / SSE | Gateway / @Sse() | socket.io 直 attach、SSE 手書き |
| バリデーション | ValidationPipe | 自前 if + throw |
| ロガー | Logger / nestjs-pino | console.log 散在 |
| 例外 | ExceptionFilter | Service 内 try-catch |
| CQRS | @nestjs/cqrs | 自前 EventBus |
| OpenAPI | @nestjs/swagger | yaml 手書きコミット |
| テスト | @nestjs/testing | jest.mock で全部置換 |
| Discovery | DiscoveryService | リフレクション直書き |
| Lazy Load | LazyModuleLoader | dynamic import 直接 |
本章の要点
| # | 要点 |
|---|---|
| 1 | NestJS は 18 のモジュールを提供。自分が書いてる処理がどれかと被ってないか棚卸しする |
| 2 | CacheModule は v11 で TTL 単位がミリ秒に変更、cache-manager v6 + Keyv ベース |
| 3 | ScheduleModule は 複数 Pod で多重実行、distributed lock 別途必要 |
| 4 | ThrottlerModule は Redis storage が公式同梱されない、コミュニティ実装を使う |
| 5 | BullMQ は @nestjs/bull の後継、Worker と Queue は別 connection |
| 6 | GraphQL は DataLoader が公式に含まれない、別途必要 |
| 7 | ValidationPipe の class-validator / class-transformer は更新滞り中、Zod / typia への移行検討 |
| 8 | Logger は同期 stdout 書き込みでホットパスをブロック、Pino へ移行が定石 |
| 9 | DiscoveryService + MetadataScanner で社内独自の Annotation 駆動 Framework が書ける |
効いている根本原理
本章は 原理4(メモリ効率は DI スコープ × ストリーム × Logger 設計に集約される) の前段として、「公式が提供しているもの」を地図化した。次章では、ここで触れた標準モジュールでもカバーされていない領域 ─ pagination、retry、circuit breaker、distributed lock、tenant context、idempotency 等 ─ を準公式パッケージで埋める方法を見ていく。