◾️ はじめに
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
-
メリット:
- 機能ごとにコードがまとまっているため、直感的でわかりやすい
- 新しい機能の追加や既存機能のリファクタリングが容易
-
デメリット:
- 小規模プロジェクトでは過剰な設計になることがある
- 共通処理が多い場合、
common
やshared
ディレクトリを適切に設計する必要がある
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アプリケーションの保守性とスケーラビリティを向上させます。以下を意識すると良い設計が実現します。
- Feature-based設計を採用する
- DIを活用して疎結合を実現する
- 責務分離を徹底し、各層の役割を明確にする
これにより、スケーラブルで保守性の高いアプリケーションを構築できます!