📦

NestJSのDIの素晴らしさを3分で体感する!

に公開

本記事のサマリ

NestJSの依存性注入(DI)は、一見複雑に見えるかもしれませんが、実際に使ってみるとその簡潔さに驚くはずです。本記事では、従来のNode.jsアプリケーションでの実装と比較しながら、NestJSのDIがいかに開発者の負担を軽減してくれるかを体感していただきます。特に、NestJSが裏側で自動処理してくれる部分と、開発者が意識すべき部分を明確に分けて解説していきます!

DIって何?なぜこんなに重要なの?

依存性注入(Dependency Injection)は、オブジェクトが必要とする依存関係を外部から「注入」してもらう設計パターンです。「なんだか難しそう...」と思われるかもしれませんが、実は日常的にやっていることなんですよね。

従来のNode.jsアプリケーションでは、こんな感じでサービスクラスを使っていましたと思います。

// 従来の方法(DIなし)
import { UserRepository } from './user.repository';

class UserService {
  private userRepository: UserRepository;

  constructor() {
    // 依存関係を内部で生成
    this.userRepository = new UserRepository();
  }

  async getUser(id: string) {
    return this.userRepository.findById(id);
  }
}

この実装の問題点は、UserServiceUserRepositoryの具体的な実装に強く依存していることです。テストで別の実装に差し替えたいときや、設定に応じて異なるリポジトリを使いたいときに困ってしまいますよね。

DIを使えば、この依存関係を外部から「注入」してもらえます。しかし、手動でDIコンテナを作ろうとすると、かなりの手間がかかるのが現実です。

NestJSのDIが素敵な理由

NestJSは、Node.js/TypeScript環境において、Spring BootやASP.NET Coreのような本格的なDIコンテナを提供してくれます。公式ドキュメントでも「エンタープライズレベルのスケーラブルなアプリケーション」の構築を掲げているように、その設計思想は非常に本格的なんです。

https://docs.nestjs.com/

NestJSのDIの最大の特徴は、デコレーターベースの宣言的な記述です。開発者は「何を注入したいか」を宣言するだけで、「いつ、どのように注入するか」はフレームワークが自動的に処理してくれます。

実際に体感してみましょう!

まずは、シンプルなユーザー管理APIを例に、NestJSのDIを体感してみましょう。

サービスクラスの作成

// user.service.ts
import { Injectable } from '@nestjs/common';

@Injectable()
export class UserService {
  private users = [
    { id: '1', name: 'Alice', email: 'alice@example.com' },
    { id: '2', name: 'Bob', email: 'bob@example.com' },
  ];

  findAll() {
    return this.users;
  }

  findById(id: string) {
    return this.users.find(user => user.id === id);
  }
}

ここで注目してほしいのは @Injectable()デコレーターです。これだけで、このクラスがDIコンテナで管理される「注入可能な」サービスになります!

コントローラーでの利用

// user.controller.ts
import { Controller, Get, Param } from '@nestjs/common';
import { UserService } from './user.service';

@Controller('users')
export class UserController {
  constructor(private readonly userService: UserService) {}

  @Get()
  findAll() {
    return this.userService.findAll();
  }

  @Get(':id')
  findOne(@Param('id') id: string) {
    return this.userService.findById(id);
  }
}

コンストラクタで UserServiceを受け取っているだけですが、これで依存性注入が完了です。「え、これだけ?」と思われるかもしれませんが、本当にこれだけなんです!

モジュールでの登録

// user.module.ts
import { Module } from '@nestjs/common';
import { UserController } from './user.controller';
import { UserService } from './user.service';

@Module({
  controllers: [UserController],
  providers: [UserService],
})
export class UserModule {}

providers配列に UserServiceを登録することで、このモジュール内でDIが有効になります。

複数のサービス間でのDI

実際のアプリケーションでは、サービス同士が依存し合うことがよくありますよね。そんな場合でも、NestJSなら自然に書けます。

// email.service.ts
@Injectable()
export class EmailService {
  sendWelcomeEmail(email: string) {
    console.log(`Welcome email sent to ${email}`);
  }
}

// user.service.ts(改良版)
@Injectable()
export class UserService {
  constructor(private readonly emailService: EmailService) {}

  private users = [
    { id: '1', name: 'Alice', email: 'alice@example.com' },
    { id: '2', name: 'Bob', email: 'bob@example.com' },
  ];

  createUser(userData: CreateUserDto) {
    const newUser = { id: Date.now().toString(), ...userData };
    this.users.push(newUser);
  
    // 他のサービスを自然に利用
    this.emailService.sendWelcomeEmail(newUser.email);
  
    return newUser;
  }
}

UserServiceのコンストラクタで EmailServiceを受け取るだけで、サービス間の連携が完成です。従来の方法なら、サービスのインスタンス化やライフサイクル管理を手動で行う必要がありましたが、NestJSでは完全に自動化されています!

NestJSがブラックボックス化している魔法の部分

ここで、NestJSが裏側でどんな「魔法」をかけてくれているのかを整理してみましょう。

自動的なサービスのインスタンス管理

従来なら手動でやらなければならなかった以下の処理を、NestJSが自動で行ってくれます。

サービスのインスタンス生成タイミング制御
アプリケーション起動時に依存関係のグラフを解析し、適切な順序でサービスのインスタンスを生成してくれます。循環依存のチェックも自動で行われるので、「あれ?なんか無限ループになってる?」といった問題も防げます。

シングルトンパターンの適用
デフォルトでは、各サービスのインスタンスはシングルトンとして管理されます。つまり、アプリケーション全体で同じサービスのインスタンスが使い回されるということです。メモリ効率も良く、状態の整合性も保たれます。

型安全性の担保
TypeScriptの型情報を活用して、コンパイル時に依存関係の整合性をチェックしてくれます。存在しないサービスを注入しようとしたり、型が合わない場合は、実行前にエラーで教えてくれるんです。

💡 型情報を使った自動的な「つなぎ込み」

NestJSの真骨頂は、TypeScriptの型情報を活用した依存関係の自動解決です。デコレーターが型情報を読み取って、裏側で「このサービスにはこのサービスが必要だな」と判断してくれるんですね。

// これだけの記述で...
constructor(private readonly userService: UserService) {}

// 内部的には以下のような処理が自動実行される
// 1. UserServiceという「型」を読み取る
// 2. DIコンテナから該当するサービスのインスタンスを検索
// 3. 見つからない場合は新しく生成
// 4. コンストラクタに自動で渡す

開発者は「何を注入したいか」を宣言するだけで、「どのように注入するか」「いつ生成するか」といった複雑な処理は完全にブラックボックス化されています。この仕組みがあるからこそ、コンストラクタに型を書くだけでDIが完了するというシンプルさが実現できているわけです!

開発者が意識すべき部分

一方で、開発者として意識すべき部分もあります。これらは「ブラックボックス」ではなく、意図的に開発者の制御下に置かれている部分です。

スコープの制御

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

@Injectable({ scope: Scope.REQUEST })
export class LoggerService {
  private startTime = Date.now();
  
  log(message: string) {
    console.log(`[${Date.now() - this.startTime}ms] ${message}`);
  }
}

Scope.REQUESTを指定することで、リクエストごとに新しいサービスのインスタンスが生成されます。ログサービスのように、リクエスト固有の状態を持ちたい場合に便利ですね。

正直、私自身もこの機能は今回改めて調べて「こんな柔軟な制御ができるのか!」と驚きました。デフォルトのシングルトンで十分なケースが多いですが、リクエストごとに状態を持ちたい場合やユーザーごとに異なる設定を使いたい場合など、実務でも活用できる場面は多そうです。積極的に使っていきたいと思います!

カスタムプロバイダー

// user.module.ts
@Module({
  providers: [
    {
      provide: 'DATABASE_CONNECTION',
      useValue: createDatabaseConnection(),
    },
    UserService,
  ],
})
export class UserModule {}

文字列をトークンとして使ったり、ファクトリー関数を使ったりと、柔軟な依存関係の定義も可能です。

従来の実装と比べてみると...

従来のNode.jsアプリケーションで同様の機能を実現しようとすると、こんな感じになっていたはずです。

// 従来の手動DI(参考程度)
class DIContainer {
  private services = new Map();

  register<T>(token: string, service: T) {
    this.services.set(token, service);
  }

  get<T>(token: string): T {
    return this.services.get(token);
  }
}

// 使用側
const container = new DIContainer();
container.register('EmailService', new EmailService());
container.register('UserService', new UserService(container.get('EmailService')));

const userService = container.get('UserService');

依存関係の管理、サービスのインスタンス生成順序、ライフサイクルの制御など、すべてを手動で実装する必要がありました。NestJSを使えば、これらの複雑な処理をすべてフレームワークに任せて、ビジネスロジックに集中できるというわけです!

パフォーマンスとテスタビリティの向上

NestJSのDIシステムは、パフォーマンスとテスタビリティの両面でメリットがあります。

モックサービスの注入も非常に簡単です。

// テストでのモック注入
const mockUserService = {
  findById: jest.fn().mockResolvedValue({ id: '1', name: 'Test User' }),
};

const module = await Test.createTestingModule({
  controllers: [UserController],
  providers: [
    {
      provide: UserService,
      useValue: mockUserService,
    },
  ],
}).compile();

テスト時には本物のサービスの代わりにモックを注入できるので、単体テストが格段に書きやすくなります。

まとめ:NestJSのDIで開発がこう変わる

NestJSの依存性注入システムは、複雑な仕組みを使っているにも関わらず、開発者には非常にシンプルなAPIを提供してくれます。@Injectable()デコレーターを付けて、コンストラクタで受け取るだけで、エンタープライズレベルのDIシステムが利用できるのは本当に素敵な体験だと思います!✨

フレームワークがブラックボックス化してくれている部分(サービスのインスタンス管理、依存関係解決、ライフサイクル制御)により、開発者はビジネスロジックの実装に集中できます。一方で、スコープ制御やカスタムプロバイダーなど、必要に応じて細かく制御できる仕組みも用意されています。

Node.js/TypeScript環境で本格的なWebアプリケーションを開発するなら、NestJSのDIは是非一度体感してみてください!

https://docs.nestjs.com/providers

株式会社StellarCreate | Tech blog📚

Discussion