Lesson 6
NestJS コラム
目次
Lesson 6
Chapter 1
controller, service, repositoryの役割
このChapterではNestJSの中心を担うcontroller, service, repositoryの役割について説明します。コードは書かずに文章のみの解説になりますので手を休めて読んでください。
controller
controllerはリクエストを受け取り、serviceに処理を依頼します。serviceからのレスポンスを受け取り、レスポンスを返します。主な役割はルーティングです。リクエストされたエンドポイントに基づいて適切に処理を振り分けます。NestJSで構築するアプリケーションにおける司令塔といえます。以下の図ではエンドポイントごとにコントーローラーe存在し、それぞれが担当するエンドポイントごとにリクエストの処理を担当します。
コントローラーのイメージ
3層アーキテクチャから見るcontroller
controllerは3層アーキテクチャにおいてプレゼンテーション層を担当します。プレゼンテーション層はクライアントからの情報をビジネス層としてのservice
に受け渡す役割を持ちます。
実装から見るcontroller
今回のTodoアプリの実装ではcontrollerはリクエストに対してinterceptors
やguards
を適用する役割を持ちました。ルーティングを担当するためリクエストの窓口となります。そのためcontrollerにてリクエストへの追加処理を施します。
service
serviceはcontrollerからのリクエストを受け取り、repositoryに処理を依頼します。repositoryからデータを受け取り、repositoryから受け取ったデータを元に処理を施しcontrollerへ返します。主な役割はビジネスロジックです。ビジネスロジックとは、アプリケーション独自の要件を実装するコアロジックのことです。「ユーザーがログインする」や「商品を購入する」等アプリケーションの機能のメインとなるロジックを担当します。
serviceのイメージ
3層アーキテクチャから見るservice
serviceは3層アーキテクチャにおいてビジネス層を担当します。ビジネス層はプレゼンテーション層としてのcontroller
から受け取った情報をデータアクセス層としてのrepository
に受け渡す役割を持ちます。
実装から見るservice
今回のTodoアプリの実装ではserviceはcontrollerから渡されたリクエストデータをもとにrepositoryのメソッドを呼び出し受け取ったデータをDTOを通してcontrollerへ返しました。今回はアプリの要件がシンプルだったためserviceの処理内容が複雑にならず済みましたが、実際のアプリケーションではserviceの処理内容が複雑になることが多いです。serviceの肥大化には気をつけつつ責務を意識した実装を心がけていきましょう。
repository
repositoryはserviceから呼び出され、データベースに処理を依頼します。データベースからデータを受け取り、serviceへ返します。主な役割はデータベースへのアクセスです。データベースへのアクセスを行うためのインタフェースを提供します。ここでいうインタフェースとは「DBを抽象化したもの」のことを言います。実際のDBはMySQLやPostgreSQLなどさまざまですが、それらを抽象化したものをインタフェースと呼びます。インタフェースを通してデータベースへのアクセスを行うことで、データベースの変更に強いアプリケーションを作ることができます。
repositoryのイメージ
3層アーキテクチャから見るrepository
repositoryは3層アーキテクチャにおいてデータアクセス層を担当します。データアクセス層はビジネス層としてのservice
から受け取った情報をデータベースに受け渡す役割を持ちます。
実装から見るrepository
今回のTodoアプリの実装ではrepositoryはserviceからDIで呼び出され、DBから取得したデータを加工し、serviceへ返却していました。また、アプリケーションの要件に合わせて元のメソッドをオーバーライド(上書き)して実装を行いました。今回はORMにTypeORMを使用していたためTypeORMの機能を元にしたRepository実装を行いました。

Lesson 6
Chapter 2
moduleの役割
このChapterではNestJSの中心を担うmoduleの役割について解説します。
module
moduleはNestJSの中心となる機能です。moduleは関連するControllerやServiceなどをまとめ、アプリケーションとして利用できるようにNestJSに登録する役割を持ちます。moduleは「RootModule」と「FeatureModule」の2種類に分けられます。
moduleのイメージ
RootModuleの役割
RootModuleはアプリケーションのエントリーポイントとなるモジュールです。RootModuleはアプリケーションの起動時にNestJSへ登録されます。RootModuleの役割は他のモジュールをインポートして使用できるようにし、アプリケーションの全体を管理することです。NestJSで構築されるアプリケーションは必ず1つのRootModuleを持ちます。
FeatureModuleの役割
FeatureModuleは特定の機能を持つモジュールです。RootModuleに登録され必要に応じて使用されます。Feature
という単語には「特徴・特色」等の意味があります。プログラミングの文脈では「1つの独立した機能」を表す単語として扱われることが多いです。今回の実装で言うと「TodoModule」を作成しました。「TodoModule」はTodoのルーティングからCRUD機能を司るModuleです。CRUDとはDB操作でいう「Create・Read・Update・Delete」のことです。FeatureModuleの役割は特定の機能の振る舞いを一気通貫で実現することです。

Lesson 6
Chapter 3
Injectableの意味
このChapterではNestJSでよく用いられる「Injectable」の意味について解説します。
Injectable
InjectableはDependency Injection(依存性の注入)を行うためのデコレータです。デコレータは@Injectable()
のように@がついている関数のことを言いましたね。依存性の注入とは、クラスのインスタンスを生成する際に、そのクラスが依存しているクラスのインスタンスを自動的に生成し、クラスのインスタンスを生成する際に引数として渡すことを指します。
依存性
依存性とは、あるクラスのインスタンスの生成には他のクラスのインスタンスが必要であるという状況を指します。たとえば、TodoService
の生成にはTodoRepository
が必要です。なぜならTodoService
の処理ではTodoRepository
を利用しているからです。つまり、TodoService
を生成する際にTodoRepository
のインスタンスが必要となるためTodoService
はTodoRepository
へ依存しています。この関係は依存性が存在する状況です。
Dependency Injection(依存性の注入)
Dependency
InjectionはDIと略されます。NestJSを理解するためにもっとも大事な概念です。NestJSで作成されたアプリケーションはDIの元成り立っています。上記「依存性」の例を考えてみましょう。TodoService
はTodoRepository
に依存しているためTodoRepository
のインスタンスが必要です。実装を思い出してほしいのですが、TodoService
の実装の際TodoRepository
をTodoService
内部でインスタンス化していましたでしょうか?実際のコードを見てみるとconstructor
の引数でインスタンスを受け取っているのがわかります。
@Injectable()
export class TodoService {
constructor(private todoRepository: TodoRepository) {}
...省略
}
インスタンスは引数で受け取る
DIを実際の用途に合わせて具体的に説明するとインスタンスを外部から受け取ることを言います。なぜなら内部でインスタンスを生成すると依存性が強くなるためです。依存性が強い状況は密結合と表現されます。内部でインスタンスを生成すると状況に合わせて必要なインスタンスを変更できない等のデメリットが存在します。たとえば本番用のインスタンスとテスト用のインスタンスで元となるクラスが異なる場合インスタンス化している部分を書き換えなければいけません。それが他クラスも必要となると変更箇所の多さは容易に想像がつきます。人間はミスをするため手動でそのような大規模な変更を度々行うべきではありません。そのためにDIが存在し、NestJSがその機能を隠蔽し便利に使えるようInterface
の参照のみで我々が利用できるようになっています。また、DIが実現でき依存性が低い状況のことを疎結合と表現します。疎結合はテストや変更が容易になるため、より良いコードを書くために必要な概念です。

Lesson 6
Chapter 4
NestJSのリクエストライフサイクル
このChapterではNestJSのリクエストライフサイクルについて解説します。
リクエストライフサイクルとは
NestJSのリクエストライフサイクルを説明する前にリクエストライフサイクルとは、リクエストが発生した際にどのような処理がどのような順番で行われるかを表したものです。
NestJSのリクエストライフサイクル
上記を踏まえてNestJSのリクエストライフサイクルを図で表してみると以下のようになります。
NestJSのリクエストライフサイクル
Middleware
Middleware
はリクエストが発生した際、最初に実行される処理です。関数の実行や、リクエストのログ出力、リクエストレスポンスオブジェクト操作等の処理を行います。用途が限定されているわけではなく、ライフサイクルの中でもなんでも屋的な存在です。
処理順
Middleware
は以下の順番で実行されます。
- グローバル単位の
Middleware
- モジュール単位の
Middleware
グローバル単位のミドルウェアはNestJSで構築されたアプリケーションのなかのすべてのエンドポイントで動作します。モジュール単位のミドルウェアはFeatureModule単位で動作します。
Guards
Guards
はリクエストが発生した際、Middleware
の後に実行される処理です。Middleware
との決定的な違いとしてGuards
は明確な責務を持つことにあります。Guards
の責務は「認証・認可」の処理です。
処理順
Guards
は以下の順番で実行されます。
- グローバル単位の
Guards
- コントローラー単位の
Guards
- ルート単位の
Guards
グローバル単位のGuards
はNestJSで構築されたアプリケーションのなかのすべてのエンドポイントで動作します。コントローラー単位のGuards
は処理が振られたコントローラーの順番で処理されます。ルート単位のGuards
はコントローラーの格Guards
が処理された後コントローラーごとに処理されます。
Interceptors
Interceptors
はリクエストが発生した際、Guards
の後に実行され、レスポンスの中間でも発生する処理です。Interceptors
はメソッドが呼び出される前後のロジックをまとめたり、関数から返される値を変形したり、例外を変形したり、関数の振る舞いを拡張したりといった共通の処理をまとめて管理するために利用されます。
処理順
Interceptors
は以下の順番で実行されます。
- グローバル単位・リクエストの
Interceptors
- コントローラー単位・リクエストの
Interceptors
- ルート単位・リクエストの
Interceptors
- グローバル単位・レスポンスの
Interceptors
- コントローラー単位・レスポンスの
Interceptors
- ルート単位・レスポンスの
Interceptors
グローバル単位のInterceptors
はNestJSで構築されたアプリケーションのなかのすべてのエンドポイントで動作します。
その後コントローラー単位、ルート単位と続きます。Interceptors
は唯一リクエスト・レスポンスの両方の処理を行うことができます。
Pipes
Pipes
はリクエストが発生した際、Interceptors
の後に実行される処理です。Pipes
はGuards
と同じく明確な責務を持ちます。Pipes
の責務は「入力値のバリデーションとデータ変形」です。
処理順
Pipes
は以下の順番で実行されます。
- グローバル単位の
Pipes
- コントローラー単位の
Pipes
- ルート単位の
Pipes
- ルートパラメーター単位の
Pipes
グローバル単位のPipes
はNestJSで構築されたアプリケーションのなかのすべてのエンドポイントで動作します。
その後コントローラー単位、ルート単位と続きます。最後にルートパラメーター(queryやbody等)単位のPipes
処理が実行されます。
Exception Filters
Exception Filters
は「例外の処理」という責務を持ちます。アプリケーション内で発生する例外をすべてキャッチします。他との違いは定義されていても必ずしもライフサイクルの中で実行されないという点です。
処理順
Exception Filters
に処理順は存在せず図で示したブロックすべての例外から派生する形で処理が実行されます。
まとめ
NestJSには以上のようなリクエストライフサイクルが存在します。処理の流れを理解することでそれぞれの機能を利用する際に用途が明確になります。ライフサイクルイベントを使用する際には常に順番を意識して責務があれば責務に沿って実装するように意識していきましょう。
