NestJSアプリケーションは、Module、Controller、Providerという3つの主要なビルディングブロックで構成されています。これらの要素を正しく理解し活用することで、保守性が高く、テストしやすいアプリケーション構造を設計できます。本記事では、各要素の役割と依存性注入(DI)の仕組み、そして実践的なモジュール分割パターンを解説します。

実行環境と前提条件

本記事の内容を実践するにあたり、以下の環境を前提としています。

項目 バージョン・要件
Node.js 20以上
npm 10以上
NestJS 11.x
OS Windows / macOS / Linux
エディタ VS Code(推奨)

事前に以下の準備を完了してください。

  • NestJS CLIのインストール済み
  • NestJSプロジェクトの作成済み(nest newコマンドで作成可能)

プロジェクトの作成方法はNestJS入門記事を参照してください。

NestJSアーキテクチャの全体像

NestJSアプリケーションの構造を理解するために、まず3つの主要コンポーネントの関係を確認しましょう。

graph TD
    A[Module] --> B[Controller]
    A --> C[Provider/Service]
    B --> C
    D[Client Request] --> B
    B --> E[Response]
    C --> F[Database/External API]

各コンポーネントの役割は以下のとおりです。

コンポーネント 役割 デコレータ
Module アプリケーションの構造を定義し、関連するController・Providerをグループ化する @Module()
Controller HTTPリクエストを受け取り、レスポンスを返す @Controller()
Provider ビジネスロジックを実装し、Controllerに注入される @Injectable()

Providerとは - ビジネスロジックの実装単位

Providerは、NestJSにおけるビジネスロジックの実装単位です。Service、Repository、Factory、Helperなど、様々な形態のクラスがProviderとして機能します。

@Injectableデコレータの役割

@Injectable()デコレータを付与することで、そのクラスはNestのIoC(Inversion of Control)コンテナによって管理されます。これにより、依存性注入の対象となり、他のクラスに自動的に注入できるようになります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// users.service.ts
import { Injectable } from '@nestjs/common';

interface User {
  id: number;
  name: string;
  email: string;
}

@Injectable()
export class UsersService {
  private readonly users: User[] = [];

  create(user: Omit<User, 'id'>): User {
    const newUser = {
      id: this.users.length + 1,
      ...user,
    };
    this.users.push(newUser);
    return newUser;
  }

  findAll(): User[] {
    return this.users;
  }

  findOne(id: number): User | undefined {
    return this.users.find((user) => user.id === id);
  }

  update(id: number, updateData: Partial<User>): User | undefined {
    const userIndex = this.users.findIndex((user) => user.id === id);
    if (userIndex === -1) return undefined;

    this.users[userIndex] = { ...this.users[userIndex], ...updateData };
    return this.users[userIndex];
  }

  remove(id: number): boolean {
    const userIndex = this.users.findIndex((user) => user.id === id);
    if (userIndex === -1) return false;

    this.users.splice(userIndex, 1);
    return true;
  }
}

このServiceは、ユーザーデータのCRUD操作を担当するビジネスロジックを実装しています。@Injectable()デコレータにより、NestのDIシステムで管理され、必要な場所に自動的に注入されます。

Providerの種類と使い分け

NestJSでは、様々な種類のProviderを定義できます。

Providerの種類 用途
Service ビジネスロジックの実装 UsersServiceAuthService
Repository データアクセス層 UsersRepository
Factory オブジェクトの生成 DatabaseConnectionFactory
Helper ユーティリティ関数群 HashHelperDateHelper

Controllerとは - リクエストハンドリングの窓口

Controllerは、クライアントからのHTTPリクエストを受け取り、適切なレスポンスを返す責務を持ちます。ビジネスロジックは直接実装せず、Providerに委譲することが重要です。

@Controllerデコレータの使い方

@Controller()デコレータは、クラスをコントローラーとして定義します。引数にはルートパスのプレフィックスを指定できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
// users.controller.ts
import {
  Controller,
  Get,
  Post,
  Put,
  Delete,
  Body,
  Param,
  ParseIntPipe,
  NotFoundException,
} from '@nestjs/common';
import { UsersService } from './users.service';

// DTOの定義
class CreateUserDto {
  name: string;
  email: string;
}

class UpdateUserDto {
  name?: string;
  email?: string;
}

@Controller('users')
export class UsersController {
  constructor(private readonly usersService: UsersService) {}

  @Post()
  create(@Body() createUserDto: CreateUserDto) {
    return this.usersService.create(createUserDto);
  }

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

  @Get(':id')
  findOne(@Param('id', ParseIntPipe) id: number) {
    const user = this.usersService.findOne(id);
    if (!user) {
      throw new NotFoundException(`User with ID ${id} not found`);
    }
    return user;
  }

  @Put(':id')
  update(
    @Param('id', ParseIntPipe) id: number,
    @Body() updateUserDto: UpdateUserDto,
  ) {
    const user = this.usersService.update(id, updateUserDto);
    if (!user) {
      throw new NotFoundException(`User with ID ${id} not found`);
    }
    return user;
  }

  @Delete(':id')
  remove(@Param('id', ParseIntPipe) id: number) {
    const deleted = this.usersService.remove(id);
    if (!deleted) {
      throw new NotFoundException(`User with ID ${id} not found`);
    }
    return { message: 'User deleted successfully' };
  }
}

ControllerとServiceの責務分離

このコードでは、責務が明確に分離されています。

flowchart LR
    A[HTTP Request] --> B[Controller]
    B --> C{リクエスト処理}
    C --> D[パラメータ抽出]
    C --> E[バリデーション]
    C --> F[Service呼び出し]
    F --> G[Service]
    G --> H[ビジネスロジック]
    H --> I[データ操作]
    I --> J[結果返却]
    J --> B
    B --> K[HTTP Response]

Controllerの責務は以下に限定されています。

  • HTTPリクエストの受信とレスポンスの送信
  • リクエストパラメータの抽出(@Body()@Param()など)
  • Serviceへの処理委譲
  • エラーハンドリング(HTTPステータスコードの決定)

ビジネスロジック(データの作成・検索・更新・削除)はすべてServiceに委譲しています。

Moduleとは - アプリケーションの構造化単位

Moduleは、関連するController、Provider、その他のモジュールをグループ化し、アプリケーションの構造を定義します。すべてのNestJSアプリケーションは、少なくとも1つのルートモジュールを持ちます。

@Moduleデコレータのプロパティ

@Module()デコレータは、以下のプロパティを持つオブジェクトを受け取ります。

プロパティ 説明
imports このモジュールで必要な他のモジュールをインポートする
controllers このモジュールで定義するControllerを登録する
providers このモジュールで定義するProviderを登録する
exports 他のモジュールに公開するProviderを指定する

Feature Moduleの作成

ユーザー管理機能をカプセル化したUsersModuleを作成します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// users.module.ts
import { Module } from '@nestjs/common';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';

@Module({
  controllers: [UsersController],
  providers: [UsersService],
  exports: [UsersService], // 他のモジュールでも使用可能にする
})
export class UsersModule {}

このモジュールは、ユーザー関連のController、Serviceをカプセル化しています。exports配列にUsersServiceを追加することで、他のモジュールからこのServiceを利用できるようになります。

ルートモジュールへの統合

Feature Moduleはルートモジュール(通常はAppModule)にインポートして使用します。

1
2
3
4
5
6
7
8
9
// app.module.ts
import { Module } from '@nestjs/common';
import { UsersModule } from './users/users.module';
import { OrdersModule } from './orders/orders.module';

@Module({
  imports: [UsersModule, OrdersModule],
})
export class AppModule {}

依存性注入(DI)の仕組み

依存性注入は、NestJSの根幹をなす設計パターンです。クラスが必要とする依存関係を外部から注入することで、疎結合でテストしやすいコードを実現します。

コンストラクタ注入

NestJSでは、コンストラクタを通じて依存関係を注入します。TypeScriptの型情報を利用して、適切なインスタンスが自動的に解決されます。

1
2
3
4
5
@Controller('users')
export class UsersController {
  // privateキーワードにより、宣言と初期化を同時に行う
  constructor(private readonly usersService: UsersService) {}
}

この簡潔な構文は、以下のコードと同等です。

1
2
3
4
5
6
7
8
@Controller('users')
export class UsersController {
  private readonly usersService: UsersService;

  constructor(usersService: UsersService) {
    this.usersService = usersService;
  }
}

DIコンテナの動作フロー

NestJSのDIコンテナは、以下のフローで依存関係を解決します。

sequenceDiagram
    participant App as Application
    participant Container as IoC Container
    participant Module as Module
    participant Provider as Provider

    App->>Container: アプリケーション起動
    Container->>Module: モジュールスキャン
    Module->>Container: Provider登録情報
    Container->>Provider: インスタンス生成
    Container->>Container: 依存関係グラフ構築
    Container->>Provider: 依存関係注入
    Note over Container: すべての依存関係が解決済み
    App->>Container: リクエスト処理開始

Provider間の依存関係

Providerは他のProviderに依存できます。例えば、OrdersServiceがUsersServiceに依存する場合を考えます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// orders.service.ts
import { Injectable } from '@nestjs/common';
import { UsersService } from '../users/users.service';

interface Order {
  id: number;
  userId: number;
  product: string;
  quantity: number;
}

@Injectable()
export class OrdersService {
  private readonly orders: Order[] = [];

  constructor(private readonly usersService: UsersService) {}

  create(userId: number, product: string, quantity: number): Order | null {
    // ユーザーの存在確認
    const user = this.usersService.findOne(userId);
    if (!user) {
      return null;
    }

    const newOrder: Order = {
      id: this.orders.length + 1,
      userId,
      product,
      quantity,
    };
    this.orders.push(newOrder);
    return newOrder;
  }

  findByUser(userId: number): Order[] {
    return this.orders.filter((order) => order.userId === userId);
  }
}

この依存関係を解決するには、OrdersModuleでUsersModuleをインポートする必要があります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// orders.module.ts
import { Module } from '@nestjs/common';
import { OrdersController } from './orders.controller';
import { OrdersService } from './orders.service';
import { UsersModule } from '../users/users.module';

@Module({
  imports: [UsersModule], // UsersServiceを使用するためにインポート
  controllers: [OrdersController],
  providers: [OrdersService],
})
export class OrdersModule {}

モジュール設計のベストプラクティス

大規模アプリケーションでは、適切なモジュール設計が重要です。

機能単位でのモジュール分割

ドメイン(機能領域)ごとにモジュールを分割することで、コードの凝集度が高まり、保守性が向上します。

src/
├── app.module.ts          # ルートモジュール
├── users/
│   ├── users.module.ts
│   ├── users.controller.ts
│   ├── users.service.ts
│   └── dto/
│       ├── create-user.dto.ts
│       └── update-user.dto.ts
├── orders/
│   ├── orders.module.ts
│   ├── orders.controller.ts
│   ├── orders.service.ts
│   └── dto/
│       └── create-order.dto.ts
├── products/
│   ├── products.module.ts
│   ├── products.controller.ts
│   └── products.service.ts
└── common/
    ├── common.module.ts
    └── services/
        └── logger.service.ts

共有モジュールの作成

複数のモジュールで共通して使用するProviderは、共有モジュールとして切り出します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// common/common.module.ts
import { Module, Global } from '@nestjs/common';
import { LoggerService } from './services/logger.service';

@Global() // グローバルモジュールとして登録(インポート不要で使用可能)
@Module({
  providers: [LoggerService],
  exports: [LoggerService],
})
export class CommonModule {}

@Global()デコレータを使用すると、このモジュールを明示的にインポートしなくても、アプリケーション全体でProviderを使用できます。ただし、グローバルモジュールの乱用はアプリケーションの構造を不明確にするため、必要な場合にのみ使用してください。

モジュール間の依存関係

モジュール間の依存関係は、循環依存を避けるよう設計します。

graph TD
    A[AppModule] --> B[UsersModule]
    A --> C[OrdersModule]
    A --> D[ProductsModule]
    A --> E[CommonModule]
    C --> B
    C --> D
    B -.-> E
    C -.-> E
    D -.-> E

    style E fill:#f9f,stroke:#333

上図では、CommonModuleがグローバルモジュール(点線)として全モジュールから利用可能であり、OrdersModuleがUsersModuleとProductsModuleに依存しています。

実践:完全なモジュール構成例

学んだ内容を統合した完全なモジュール構成を確認します。

プロジェクト構造

src/
├── main.ts
├── app.module.ts
├── common/
│   ├── common.module.ts
│   └── services/
│       └── logger.service.ts
├── users/
│   ├── users.module.ts
│   ├── users.controller.ts
│   ├── users.service.ts
│   └── interfaces/
│       └── user.interface.ts
└── orders/
    ├── orders.module.ts
    ├── orders.controller.ts
    ├── orders.service.ts
    └── interfaces/
        └── order.interface.ts

各ファイルの実装

LoggerServiceの実装例を示します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// common/services/logger.service.ts
import { Injectable } from '@nestjs/common';

@Injectable()
export class LoggerService {
  log(context: string, message: string): void {
    const timestamp = new Date().toISOString();
    console.log(`[${timestamp}] [${context}] ${message}`);
  }

  error(context: string, message: string, trace?: string): void {
    const timestamp = new Date().toISOString();
    console.error(`[${timestamp}] [${context}] ERROR: ${message}`);
    if (trace) {
      console.error(trace);
    }
  }

  warn(context: string, message: string): void {
    const timestamp = new Date().toISOString();
    console.warn(`[${timestamp}] [${context}] WARN: ${message}`);
  }
}

ルートモジュールの構成例を示します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// app.module.ts
import { Module } from '@nestjs/common';
import { CommonModule } from './common/common.module';
import { UsersModule } from './users/users.module';
import { OrdersModule } from './orders/orders.module';

@Module({
  imports: [
    CommonModule,  // グローバルモジュール(最初にインポート)
    UsersModule,
    OrdersModule,
  ],
})
export class AppModule {}

動作確認

アプリケーションを起動して動作を確認します。

1
npm run start:dev

以下のエンドポイントでAPIをテストできます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# ユーザー作成
curl -X POST http://localhost:3000/users \
  -H "Content-Type: application/json" \
  -d '{"name": "John Doe", "email": "john@example.com"}'

# ユーザー一覧取得
curl http://localhost:3000/users

# 特定ユーザー取得
curl http://localhost:3000/users/1

期待される結果として、ユーザー作成時にはIDが自動付与されたユーザーオブジェクトが返却され、一覧取得時には登録済みの全ユーザーが配列で返却されます。

まとめ

本記事では、NestJSアプリケーションの3つの柱であるModule、Controller、Providerについて解説しました。

学んだ内容を整理します。

  • Provider@Injectable()デコレータでマークされ、ビジネスロジックを実装するクラス
  • Controller@Controller()デコレータでマークされ、HTTPリクエストを処理するクラス
  • Module@Module()デコレータでマークされ、関連するController・Providerをグループ化するクラス
  • 依存性注入:コンストラクタを通じて依存関係を自動的に解決する仕組み
  • モジュール設計:機能単位での分割と共有モジュールの活用が重要

これらの概念を正しく理解し活用することで、保守性が高く、テストしやすいNestJSアプリケーションを構築できます。次のステップとして、HTTPメソッドデコレータを活用したREST APIの実装や、Pipe、Guard、Interceptorなどのミドルウェア機能について学習を進めてください。

参考リンク