0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【初心者向け】Nest.jsのアーキテクチャを図解で理解する - モジュール、コントローラー、サービスの関係性

Posted at

はじめに

Node.jsのバックエンドフレームワークとして人気の高いNest.js。Angularにインスパイアされた設計で、TypeScriptファーストな開発体験を提供しています。

しかし、初めてNest.jsに触れる方にとって「モジュール」「コントローラー」「サービス」といった概念の関係性は少し複雑に感じるかもしれません。

この記事では、Nest.jsの核となるアーキテクチャを図解を交えながら、わかりやすく解説していきます。

Nest.jsアーキテクチャの全体像

Nest.jsは以下のような階層構造でアプリケーションを構成します:

ルートモジュール (AppModule)
    ↓
機能モジュール (UserModule, ProductModule...)
    ↓
コントローラー/リゾルバー (HTTP/GraphQL)
    ↓
サービス (ビジネスロジック)

各レイヤーには明確な責務があり、この分離により保守性の高いアプリケーションを構築できます。

1. ルートモジュール - アプリケーションの司令塔

ルートモジュール(AppModule) は、アプリケーション全体の「本社」のような存在です。

身近な例で理解する

会社組織で例えると:

  • ルートモジュール = 本社(全体を統括)
  • 機能モジュール = 各部署(営業部、開発部、経理部など)
// 本社(ルートモジュール)が各部署を管理
@Module({
  imports: [
    UserModule,     // ユーザー管理部署
    ProductModule,  // 商品管理部署
    OrderModule,    // 注文管理部署
    AuthModule      // 認証管理部署
  ],
  controllers: [AppController],  // 本社直属の窓口
  providers: [AppService],       // 本社直属のサービス
})
export class AppModule {}

ルートモジュールの役割

  • 統括管理: 全ての機能モジュールを束ねる
  • アプリ起動: Nest.jsがアプリを起動する際の出発点
  • グローバル設定: データベース接続、環境設定など全体に関わる設定

2. 機能モジュール - 専門部署のような存在

機能モジュール は、特定の業務を担当する「部署」のようなものです。

具体的な例:ECサイトの場合

// ユーザー管理部署(UserModule)
@Module({
  imports: [
    TypeOrmModule.forFeature([User]), // ユーザーデータベース
    JwtModule.register({...})         // JWT認証
  ],
  controllers: [UserController],      // ユーザー関連のAPI窓口
  providers: [UserService],           // ユーザー業務処理
  exports: [UserService],             // 他部署でも利用可能にする
})
export class UserModule {}

// 商品管理部署(ProductModule)
@Module({
  imports: [
    TypeOrmModule.forFeature([Product]),
    CloudinaryModule                   // 画像アップロード
  ],
  controllers: [ProductController],    // 商品関連のAPI窓口
  providers: [ProductService],         // 商品業務処理
  exports: [ProductService],
})
export class ProductModule {}

// 注文管理部署(OrderModule)
@Module({
  imports: [
    TypeOrmModule.forFeature([Order]),
    UserModule,        // ユーザー部署と連携
    ProductModule,     // 商品部署と連携
    PaymentModule      // 決済部署と連携
  ],
  controllers: [OrderController],
  providers: [OrderService],
})
export class OrderModule {}

機能モジュールの特徴

1. 専門性

各モジュールは特定の業務領域に特化

  • UserModule → ユーザー登録、ログイン、プロフィール管理
  • ProductModule → 商品登録、在庫管理、カテゴリ管理
  • OrderModule → 注文処理、注文履歴、配送管理

2. 独立性

各モジュールは独立して動作可能

// 商品モジュールだけでも完結した機能を提供
@Get('products')
getProducts() {
  return this.productService.findAll(); // 他のモジュールに依存しない
}

3. 連携性

必要に応じて他のモジュールと連携

// 注文作成時は複数のモジュールと連携
async createOrder(userId: number, productId: number) {
  const user = await this.userService.findById(userId);       // ユーザーモジュール
  const product = await this.productService.findById(productId); // 商品モジュール
  
  // 注文処理...
}

実際のフォルダ構成

src/
├── app.module.ts              # ルートモジュール(本社)
├── main.ts                    # アプリケーション起動
└── modules/
    ├── user/                  # ユーザー管理部署
    │   ├── user.module.ts
    │   ├── user.controller.ts
    │   ├── user.service.ts
    │   └── entities/user.entity.ts
    ├── product/               # 商品管理部署
    │   ├── product.module.ts
    │   ├── product.controller.ts
    │   ├── product.service.ts
    │   └── entities/product.entity.ts
    └── order/                 # 注文管理部署
        ├── order.module.ts
        ├── order.controller.ts
        ├── order.service.ts
        └── entities/order.entity.ts

モジュールの設定項目を理解する

モジュールの@Module()デコレータには、以下の重要なプロパティがあります:

@Module({
  imports: [...],      // 他のモジュールを取り込む
  controllers: [...],  // このモジュールのAPI窓口
  providers: [...],    // このモジュールのサービス類
  exports: [...]       // 他のモジュールに提供する機能
})
export class UserModule {}

imports - 他のモジュールを取り込む

imports: [
  TypeOrmModule.forFeature([User]),  // データベース機能
  JwtModule.register({...}),         // JWT認証機能
  EmailModule,                       // メール送信機能
  ProductModule                      // 商品管理機能
]
  • 役割: 他のモジュールの機能を使用できるようにする
  • : ユーザーモジュールで商品情報が必要な場合、ProductModuleをimport

controllers - API窓口の定義

controllers: [
  UserController,     // /users のAPI処理
  AuthController      // /auth のAPI処理
]
  • 役割: HTTPリクエストを受け取るエンドポイントを定義
  • : GET /users, POST /users/login などのAPIを処理

providers - サービス類の登録

providers: [
  UserService,        // ユーザー業務処理
  UserRepository,     // データベース操作
  EmailService,       // メール送信処理
  {
    provide: 'CONFIG',
    useValue: { apiKey: 'xxx' }  // 設定値の注入
  }
]
  • 役割: このモジュール内で使用するサービスやクラスを登録
  • : ビジネスロジックを実装したサービスクラス

exports - 他のモジュールに機能を提供

exports: [
  UserService,        // 他のモジュールからも利用可能
  UserRepository      // 他のモジュールからも利用可能
]
  • 役割: このモジュールの機能を他のモジュールでも使えるようにする
  • : OrderModuleでUserServiceを使いたい場合、UserModuleでexportが必要

実際の連携例

// UserModule: ユーザー機能を他のモジュールに提供
@Module({
  imports: [TypeOrmModule.forFeature([User])],
  controllers: [UserController],
  providers: [UserService],
  exports: [UserService]  // 👈 他のモジュールでも使用可能
})
export class UserModule {}

// OrderModule: UserModuleの機能を利用
@Module({
  imports: [
    TypeOrmModule.forFeature([Order]),
    UserModule  // 👈 UserServiceを使用可能になる
  ],
  controllers: [OrderController],
  providers: [OrderService]
})
export class OrderModule {}

// OrderService内でUserServiceを使用
@Injectable()
export class OrderService {
  constructor(
    private userService: UserService  // 👈 自動的に注入される
  ) {}
}

なぜこの構成が良いのか?

1. チーム開発に最適

  • 各チームが担当モジュールに集中できる
  • 他チームの作業に影響されにくい
  • コードの衝突が起きにくい

2. 保守性の向上

  • 機能追加時、該当モジュールだけを修正
  • バグ修正の影響範囲が限定的
  • テストも部署単位で実施可能

3. 再利用性

  • 他のプロジェクトでもモジュール単位で再利用
  • マイクロサービス化への移行も容易

3. コントローラー - HTTPリクエストの窓口

コントローラー はHTTPリクエストを受け取り、適切なレスポンスを返す役割を担います。

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

  @Get()
  async findAll(): Promise<User[]> {
    return this.userService.findAll();
  }

  @Post()
  async create(@Body() createUserDto: CreateUserDto): Promise<User> {
    return this.userService.create(createUserDto);
  }

  @Get(':id')
  async findOne(@Param('id') id: string): Promise<User> {
    return this.userService.findOne(+id);
  }
}

役割

  • HTTPリクエストのルーティング
  • リクエストデータの検証
  • サービス層への処理委譲
  • レスポンスの整形

4. リゾルバー - GraphQLの橋渡し役

GraphQLを使用する場合は、リゾルバー がクエリやミューテーションを処理します。

@Resolver(() => User)
export class UserResolver {
  constructor(private readonly userService: UserService) {}

  @Query(() => [User])
  async users(): Promise<User[]> {
    return this.userService.findAll();
  }

  @Mutation(() => User)
  async createUser(
    @Args('createUserInput') createUserInput: CreateUserInput
  ): Promise<User> {
    return this.userService.create(createUserInput);
  }
}

役割

  • GraphQLクエリ/ミューテーションの処理
  • GraphQLスキーマの定義
  • サービス層への処理委譲

5. サービス - ビジネスロジックの心臓部

サービス はアプリケーションの核となるビジネスロジックを実装します。

@Injectable()
export class UserService {
  constructor(
    @InjectRepository(User)
    private userRepository: Repository<User>,
  ) {}

  async findAll(): Promise<User[]> {
    return this.userRepository.find();
  }

  async create(createUserDto: CreateUserDto): Promise<User> {
    // ビジネスロジック
    const existingUser = await this.userRepository.findOne({
      where: { email: createUserDto.email }
    });
    
    if (existingUser) {
      throw new ConflictException('Email already exists');
    }

    const user = this.userRepository.create(createUserDto);
    return this.userRepository.save(user);
  }

  async findOne(id: number): Promise<User> {
    const user = await this.userRepository.findOne({ where: { id } });
    if (!user) {
      throw new NotFoundException('User not found');
    }
    return user;
  }
}

役割

  • ビジネスロジックの実装
  • データベースとの連携
  • 他サービスとの協調
  • 例外処理

データフローの全体像

実際のリクエスト処理は以下の流れで進みます:

1. クライアント → HTTP Request
2. コントローラー → リクエスト受信・検証
3. サービス → ビジネスロジック実行
4. データベース → データ操作
5. サービス → 結果をコントローラーに返却
6. コントローラー → HTTP Response
7. クライアント → レスポンス受信

依存性注入(DI)の威力

Nest.jsの強力な機能の一つが 依存性注入(Dependency Injection) です。

// サービスをコントローラーに自動注入
@Controller('users')
export class UserController {
  constructor(
    private readonly userService: UserService,  // 自動注入
    private readonly emailService: EmailService // 自動注入
  ) {}
}

DIのメリット

  • 疎結合: クラス間の依存関係が緩やか
  • テスタビリティ: モックオブジェクトの注入が簡単
  • 保守性: 実装の変更が他に影響しにくい

実践的な設計パターン

フォルダ構成例

src/
├── app.module.ts
├── main.ts
└── modules/
    ├── user/
    │   ├── user.module.ts
    │   ├── user.controller.ts
    │   ├── user.service.ts
    │   ├── user.entity.ts
    │   └── dto/
    │       ├── create-user.dto.ts
    │       └── update-user.dto.ts
    └── product/
        ├── product.module.ts
        ├── product.controller.ts
        └── product.service.ts

モジュール間の連携

@Module({
  imports: [UserModule], // UserServiceを利用可能に
  controllers: [OrderController],
  providers: [OrderService],
})
export class OrderModule {}

@Injectable()
export class OrderService {
  constructor(
    private readonly userService: UserService, // 他モジュールのサービス注入
  ) {}
}

まとめ

Nest.jsのアーキテクチャは一見複雑に見えますが、各コンポーネントの役割を理解すれば、非常に合理的な設計であることがわかります。

  • ルートモジュール: アプリケーション全体の統合
  • モジュール: 機能の境界とスコープ管理
  • コントローラー/リゾルバー: リクエスト処理の窓口
  • サービス: ビジネスロジックの実装

この階層構造により、大規模なアプリケーションでも保守性を保ちながら開発できます。

次回は実際にCRUD APIを構築しながら、これらのコンポーネントを実装する方法を解説予定です!


参考リンク

#NestJS #TypeScript #Node.js #バックエンド #アーキテクチャ

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?