はじめに
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 #バックエンド #アーキテクチャ