皆さん、こんにちは。
今回は「NestJS の依存性注入(DI)機構を効果的に利用するためのベストプラクティス」について紹介させていただきます。
NestJS の依存性注入(DI)機構を効果的に利用するためのベストプラクティスは、アプリケーションの拡張性やテストの容易さ、保守性を向上させるために重要です。
主要なポイント
-
@Injectable()
デコレーターの適用
DI コンテナにクラスを認識させるため、サービスやプロバイダーには必ず @Injectable() デコレーターを付与します。
クラスが DI の対象となり、NestJS による自動管理が可能になります。 -
コンストラクタインジェクションの利用
依存関係はコンストラクタ経由で注入することが推奨されます。
依存性が明示的になり、ユニットテストの際にモックの注入が容易になります。 -
明確なプロバイダー定義とスコープ管理
各モジュールで必要なプロバイダーを明確に定義し、適切なスコープ(デフォルトはシングルトン、必要に応じてトランジェントやリクエストスコープも)を設定します。
アプリケーションのパフォーマンスやリソース管理が向上します。 -
サービスの疎結合設計
DI を利用してサービス間の依存関係を疎結合に保ち、直接的な new キーワードの使用を避けます。
再利用性が高く、将来的な変更にも柔軟に対応できる設計になります。 -
カスタムプロバイダーとインターフェースの活用
useClass
、useValue
、useFactory
といったオプションを利用して、依存性の提供方法を柔軟に設定します。また、インターフェースやカスタムトークンを使って依存関係の契約を明確にするのも有効です。
依存性の切り替えやモックの利用が容易になり、テストや拡張性の面でメリットがあります。 -
循環依存性の回避
複数のクラス間で循環参照が発生すると問題が生じるため、必要に応じて forwardRef() を利用するか、設計の見直しを行います。
DI コンテナによる正しい依存関係の解決が行われ、予期しない動作を防止できます。 -
テスト可能な設計
DI を活用することで、ユニットテストや統合テストにおいて依存関係を容易に差し替えることが可能です。テスト用のモックやスタブを注入できる設計を心がけます。
コードのテストカバレッジが向上し、バグの早期発見と修正が促進されます。
実装例とその解説
各コードブロックには、@Injectable() の利用、コンストラクタインジェクション、カスタムプロバイダーの設定、そして循環依存性の回避(forwardRef の利用)など、具体的な実装例とその解説を記載しています。
基本的な依存性注入
サービスとリポジトリの実装
- user.repository.ts
リポジトリはデータ取得の責務を持ち、@Injectable() を付与して DI コンテナに登録します。
// user.repository.ts
import { Injectable } from '@nestjs/common';
@Injectable()
export class UserRepository {
findAll(): string[] {
// ユーザーデータを取得するロジック(例として静的データを返す)
return ['user1', 'user2', 'user3'];
}
}
- user.service.ts
サービスはリポジトリに依存し、コンストラクタインジェクションで依存性を受け取ります。
// user.service.ts
import { Injectable } from '@nestjs/common';
import { UserRepository } from './user.repository';
@Injectable()
export class UserService {
// コンストラクタインジェクションにより依存性を明示的に注入
constructor(private readonly userRepository: UserRepository) {}
getAllUsers(): string[] {
return this.userRepository.findAll();
}
}
- app.module.ts
モジュール内でプロバイダーを定義し、必要に応じてエクスポートします。
// app.module.ts
import { Module } from '@nestjs/common';
import { UserService } from './user.service';
import { UserRepository } from './user.repository';
@Module({
providers: [UserService, UserRepository],
exports: [UserService],
})
export class AppModule {}
カスタムプロバイダーとインターフェースの活用
設定値をカスタムプロバイダーで提供する例
- config.constants.ts
DI で利用するトークンを定義します。
// config.constants.ts
export const CONFIG_TOKEN = 'CONFIG_TOKEN';
- config.provider.ts
useValue
を利用して設定値を直接提供するカスタムプロバイダーを定義します。
// config.provider.ts
import { Provider } from '@nestjs/common';
import { CONFIG_TOKEN } from './config.constants';
export const ConfigProvider: Provider = {
provide: CONFIG_TOKEN,
useValue: { apiKey: '1234567890' }, // 設定値を直接提供
};
- some.service.ts
DI により、設定値が注入されます。インジェクショントークンを使って依存関係を明確にしています。
// some.service.ts
import { Injectable, Inject } from '@nestjs/common';
import { CONFIG_TOKEN } from './config.constants';
@Injectable()
export class SomeService {
constructor(@Inject(CONFIG_TOKEN) private config: { apiKey: string }) {}
getApiKey(): string {
return this.config.apiKey;
}
}
- config.module.ts
カスタムプロバイダーとサービスをモジュールに登録します。
// config.module.ts
import { Module } from '@nestjs/common';
import { ConfigProvider } from './config.provider';
import { SomeService } from './some.service';
@Module({
providers: [ConfigProvider, SomeService],
exports: [SomeService],
})
export class ConfigModule {}
循環依存性の回避(forwardRef の利用)
相互依存するサービス間の循環依存性解消
- foo.service.ts
BarService との相互依存を解消するために forwardRef() を使用しています。
// foo.service.ts
import { Injectable, Inject, forwardRef } from '@nestjs/common';
import { BarService } from './bar.service';
@Injectable()
export class FooService {
constructor(
@Inject(forwardRef(() => BarService))
private readonly barService: BarService,
) {}
callBar(): string {
return this.barService.barMethod();
}
}
- bar.service.ts
同様に、FooService を参照する際にも forwardRef() を使用します。
// bar.service.ts
import { Injectable, Inject, forwardRef } from '@nestjs/common';
import { FooService } from './foo.service';
@Injectable()
export class BarService {
constructor(
@Inject(forwardRef(() => FooService))
private readonly fooService: FooService,
) {}
barMethod(): string {
return 'Bar method called';
}
}
- circular.module.ts
両サービスをモジュールに登録し、相互依存性を管理します。
// circular.module.ts
import { Module } from '@nestjs/common';
import { FooService } from './foo.service';
import { BarService } from './bar.service';
@Module({
providers: [FooService, BarService],
exports: [FooService, BarService],
})
export class CircularModule {}
まとめ
- @Injectable() の利用: 各クラスにデコレーターを付与することで、DI コンテナに登録します。
- コンストラクタインジェクション: 依存性を明示的に受け取ることで、テストが容易になります。
- カスタムプロバイダー: useValue、useClass、useFactory を用いて柔軟に依存性を提供できます。
- 循環依存性の回避: forwardRef() を利用して、相互依存関係の問題を解決します。
これらのベストプラクティスを意識することで、NestJS アプリケーションはより柔軟でメンテナブルな構造となり、スケーラブルな開発が実現できます。
今日は以上です。
ありがとうございました。
よろしくお願いいたします。