9
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?

GENEROSITYAdvent Calendar 2024

Day 20

Nest.jsでモジュール設計を効率化するベストプラクティス💡

Last updated at Posted at 2024-12-19

◾️ はじめに

 Nest.jsは、モジュール中心に設計されているため、モジュール構造がプロジェクトの可読性や拡張性に大きく影響します。本記事では、モジュールの設計の効率化をテーマに、設計のベストプラクティスと注意点を解説します。

◾️ 1. モジュール設計の基本

 Nest.jsでは、すべての機能をモジュール単位で管理します。
 モジュール設計とは、アプリケーションを適切な単位で分割し、それぞれの責務を明確にすることです。

モジュールの役割

  • 機能のカプセル化:機能ごとに分割し、責務を限定する
  • 再利用性の向上:特的の機能を別のプロジェクトや他のモジュールで再利用可能にする
  • 依存関係の管理:モジュール間の依存関係を明確にし、循環依存を防ぐ

循環依存とは、複数のモジュールやクラス、関数が互いに依存しあい、それが循環している状態をさします。この状態になると、コードの理解や保守が難しくなるほか、実行時エラーやコンパイルエラーの原因になり得ます。

例:UserModule

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

◾️ 2. Feature-based vs Layer-basedの違いとマイベストプラクティス💡

Feature-based

  • 特徴:1つのモジュールが1つの機能を担当する
  • 構造例
|- src
    |-user
        |-user.controller.ts
        |-user.service.ts
        |-user.repository.ts
        |-user.module.ts
    |-auth
        |-auth.controller.ts
        |-auth.service.ts
        |-auth.module.ts
        |-auth.strategy.ts
  • メリット
    • 機能ごとにコードがまとまっているため、直感的でわかりやすい
    • 新しい機能の追加や既存機能のリファクタリングが容易
  • デメリット
    • 小規模プロジェクトでは過剰な設計になることがある
    • 共通処理が多い場合、commonsharedディレクトリを適切に設計する必要がある

Layer-based

  • 特徴:役割ごとにモジュールを分割
  • 構造例
|-src
    |-controllers
        |-user.controller.ts
        |-auth.controller.ts
    |-services
        |-user.service.ts
        |-auth.service.ts
    |-repository
        |-user.repository.ts
  • メリット
    • 同じ役割をもつコードを一箇所に集約できる
  • デメリット
    • 機能ごとの依存関係が不明確
    • 拡張性が低く、大規模プロジェクトには不向き

マイベストプラクティス💡

やはり、Feature-basedをオススメします!
1つのモジュールが1つの機能を担当するため、依存関係が明確になり、機能を追加するときに一連の流れでコードを書きやすくなるからです✨

◾️ 3. DI(依存性注入)を活用した設計の効率化

依存注入(DI)とは

 DI(Dependency Injection)とは、クラスやモジュールが必要とする依存関係を自分で作成するのではなく、外部から提供してもらう設計手法です。
Nest.jsでは、この仕組みをフレームワークが自動的に管理してくれるため、モジュール間の依存関係を効率的に構築できます。

DIのメリット

1. モジュール間の結合度を低減
DIでは、クラスが必要な依存関係を自分で作成せず、外部から渡してもらう仕組みを使うことで疎結合を実現します。
2. テストが行いやすい
依存関係を外部から注入することで、モックを使用したテストが簡単になります。

DIの活用例

以下の例では、AuthServiceをUserServiceに依存として注入しています。

@Injectable()
export class UserService {
  // DIで依存を注入
  constructor(private readonly authService: AuthService) {}

  async createUser(dto: CreateUserDto) {
    const email = this.authService.validate(dto.email);
    // ユーザー作成ロジック
  }
}

テスト時には、以下のようにモックを渡すことで依存関係を模倣します。

const mockAuthService = { validate: jest.fn(() => true) };
const userService = new UserService(mockAuthService as AuthService);

expect(userService.createUser(dto)).toBeDefined();

◾️ 4. アンチパターンとその回避方法

1. 全てをGlobalモジュールにする

  • 問題点:モジュール間の依存関係が不明確になる。
  • 解決策:必要最小限のモジュールのみGlobalに設定する。

2. 無制限な依存関係の追加

  • 問題点:複雑なモジュール間の依存関係が増える。
  • 解決策:各モジュールの責務を明確にし、共通部分をSharedモジュールに切り出します。

3. 巨大なモジュール

  • 問題点:1つのモジュールに多くの責務が集中する。
  • 解決策:機能ごとにモジュールを分割する。

◾️ 5. 責務の分離とレイヤー設計の重要性

 責務を分離することで、以下のようなメリットが得られます。

  • 保守性の向上
    変更に強い設計が可能になります。
  • 再利用性の向上
    共通ロジックをレポジトリ層で再利用可能にします。
  • テストの効率化
    層ごとにモックを置き換えることで、テストが容易になります。

責務の分離の基本

サービス層(Service Layer)

  • ビジネスロジックを担当
  • データベースへの直接的なアクセスは行わず、レポジトリ層を通じてデータを取得・保存
    例:ユーザーの登録処理やバリデーションの実装を行う

レポジトリ層(Repository Layer)

  • データベースとのやり取りを専任で行う
  • ORM(PrismaやTypeORMなど)を利用してCRUD操作を実装
  • データアクセスロジックの共通化・再利用を目的とする

責務分離の実装例

以下に、責務を分離した具体的なコード例を示します。

UserRepository
レポジトリ層でデータベースとのやり取りをカプセル化します。

import { Injectable } from '@nestjs/common';
// Prismaを利用した例
import { PrismaService } from '../prisma/prisma.service'; 

@Injectable()
export class UserRepository {
  constructor(private readonly prisma: PrismaService) {}

  async findById(id: string) {
    return this.prisma.user.findUnique({ where: { id } });
  }

  async create(data: { name: string; email: string }) {
    return this.prisma.user.create({ data });
  }
}

UserService
サービス層では、ビジネスロジックに集中します。データベースの操作はレポジトリ層に委譲します。

import { Injectable, NotFoundException } from '@nestjs/common';
import { UserRepository } from './user.repository';

@Injectable()
export class UserService {
  constructor(private readonly userRepository: UserRepository) {}

  async getUserById(id: string) {
    const user = await this.userRepository.findById(id);
    if (!user) {
      throw new NotFoundException('User not found');
    }
    return user;
  }

  async registerUser(name: string, email: string) {
    // ビジネスロジックの追加(例: 重複チェックなど)
    const user = await this.userRepository.create({ name, email });
    return user;
  }
}

UserModule
モジュール内で、依存性を注入するための設定を行います。

import { Module } from '@nestjs/common';
import { UserService } from './user.service';
import { UserRepository } from './user.repository';

@Module({
  providers: [UserService, UserRepository],
  exports: [UserService], // 他のモジュールで利用する場合
})
export class UserModule {}

◾️ 7. 責務分離を踏まえたモジュール設計

最終的には、以下のようなモジュール設計を目指します。

構造例

|-src
    |-user
        |-user.controller.ts   // HTTPリクエストを処理
        |-user.service.ts      // ビジネスロジック
        |-user.repository.ts   // データベース操作
        |-user.module.ts       // 依存性の設定
    |-prisma
        |-prisma.service.ts    // データベース接続管理

◾️ まとめ

効率的なモジュール設計は、Nest.jsアプリケーションの保守性とスケーラビリティを向上させます。以下を意識すると良い設計が実現します。

  1. Feature-based設計を採用する
  2. DIを活用して疎結合を実現する
  3. 責務分離を徹底し、各層の役割を明確にする

これにより、スケーラブルで保守性の高いアプリケーションを構築できます!

9
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
9
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?