ソースコードで理解するNestJS②: メタデータの利用

# 前回のまとめなど

https://qiita.com/fj-th/items/5c11251bc66feb477f09

前回の記事では、`@Controller`デコレータの実装から、NestJSの各種組み込みデコレータが対象のクラスやメソッドにReflectionによるメタデータを付与することを責務としていることを見ました。

デコレータがメタデータの付与に特化しているため、実際のルーティングやDIといった機能は、これらのメタデータを利用するコンポーネントが実現します。今回の記事では、特に`@Controller`で付与されたメタデータがどのように利用され、ルーティングを実現しているのかを見ていきます。

#### 該当するソースコード

https://github.com/nestjs/nest/tree/e8152d1bbf541a979152285455373801c6d8e692/packages/core/router


# `@Controller`のおさらい
前回見た`@Controller`のソースコードのおさらいです。

```typescript
export function Controller(
  prefixOrOptions?: string | string[] | ControllerOptions,
): ClassDecorator {

  //  ------------------------------------------------------
  //  省略: 引数に応じたpath, host, scopeOptions, versionOptionsの設定
  //  ------------------------------------------------------
  
  return (target: object) => {
    Reflect.defineMetadata(CONTROLLER_WATERMARK, true, target);
    Reflect.defineMetadata(PATH_METADATA, path, target);
    Reflect.defineMetadata(HOST_METADATA, host, target);
    Reflect.defineMetadata(SCOPE_OPTIONS_METADATA, scopeOptions, target);
    Reflect.defineMetadata(VERSION_METADATA, versionOptions, target);
  };
}

```
[reflect-metadata](https://github.com/rbuckton/reflect-metadata)の `Reflect.defineMetadata`を用いて、
- `CONTROLLER_WATERMARK`
- `PATH_METADATA`
- `HOST_METADATA`
- `SCOPE_OPTIONS_METADATA`
- `VERSION_METADATA`

という　5つのメタデータを対象のクラスに付与しています。これらのメタデータキーのそれぞれについて見ていくことにします。なお、メタデータの全体は以下で定義されています。

https://github.com/nestjs/nest/blob/e8152d1bbf541a979152285455373801c6d8e692/packages/common/constants.ts#L44


# `PATH_METADATA`
最初に最も重要なものを見ることにします。本メタデータはなんとなく予想が着くでしょう。ルートパスを保持しています。`@Controller`の引数で指定された`path`が直接格納されることからして、ルーティングを理解する上で最も重要なのは間違いなくこれです。

::: note info
`PATH_METADATA`メタデータはコントローラーやそのメソッドに設定されたルートパスを表す
:::

本メタデータの利用者(`Reflect.getMetadata`の実行箇所)として重要なのは[`RouterExplorer`](https://github.com/nestjs/nest/blob/e8152d1bbf541a979152285455373801c6d8e692/packages/core/router/router-explorer.ts#L120).`extractRouterPath`で、設定されたメタデータからpathを取得し、返却しています。そして、この`extractRouterPath`を利用しているのは[`RoutesResolver`](https://github.com/nestjs/nest/blob/e8152d1bbf541a979152285455373801c6d8e692/packages/core/router/routes-resolver.ts#L95).`registerRouters`で、これがNestJSにおけるルーティング処理の中心となっているようです。

 ```typescript
public registerRouters(
  routes: Map<string | symbol | Function, InstanceWrapper<Controller>>,
  moduleName: string,
  globalPrefix: string,
  modulePath: string,
  applicationRef: HttpServer,
) {
  // ルートマップ内の各コントローラをループ処理
  routes.forEach(instanceWrapper => {

    //省略
    
    // **************************************************
    // 2. コントローラに関連するルートパスを抽出
    const routerPaths = this.routerExplorer.extractRouterPath(
      metatype as Type<any>,
    );
    // **************************************************

    // 省略

    // 各ルートパスを処理
    routerPaths.forEach(path => {
      // グローバルプレフィックスやモジュールパスを使用して最終的なパスを生成
      const pathsToLog = this.routePathFactory.create({
        ctrlPath: path,
        modulePath,
        globalPrefix,
      });

      // 省略
      const routePathMetadata: RoutePathMetadata = {
          ctrlPath: path,
          modulePath,
          globalPrefix,
          controllerVersion,
          versioningOptions,
      };
      // ルートの探索と登録を実行
      this.routerExplorer.explore(
        instanceWrapper,
        moduleName,
        applicationRef,
        host,
        routePathMetadata,
      );
    });
  });
}
 ```

ルーティングの核心に迫るため、`this.routerExplorer.explore`に潜ってみましょう。ここで各ルートをHTTPサーバーにバインドし、実際のリクエストに対して適切なコントローラメソッドが呼び出されるように処理されているはずです。ここで、ルーターとは`HttpServer`インターフェースを持つもの、例えば　`express.Router`などになります。

## ルーターへの登録
`this.routerExplorer.explore`の処理は以下のように進みます。

- `this.routerExplorer.explore`
コントローラのインスタンスのメタデータを調べ、関連するルートパス情報を組み立てる
- -> `RouterExplorer#applyPathsToRouterProxy`
ルートパスのそれぞれに対して以下を実行する
- -> `RouterExplorer#applyCallbackToRouter`
与えられたルートパスに対してリクエストハンドラをルーターに登録

そして、`applyCallbackToRouter`は、各ルート定義に対して以下の処理を行います。

#### routerMethodRef の取得
`RouterMethodFactory`を使用して、指定された HTTP メソッドに対応するルーティング関数を取得し、router (HttpServerインターフェース... 実態としてはexpress.Routerなど) にバインドします。これにより、後続の処理で `routerMethodRef` が正しい HTTP メソッドに基づいて呼び出されます。

```typescript
    const routerMethodRef = this.routerMethodFactory
          .get(router, requestMethod)
          .bind(router);
```
#### プロキシハンドラの作成
`createCallbackProxy`によりルートハンドラをラップし、実行時コンテキストやエラーハンドリングを含む、「リクエストに対して実際に実行されるメソッド = プロキシ」を作成します。また、ここでスコープに関する条件分岐が入っています。

```typescript
const proxy = isRequestScoped
      ? this.createRequestScopedHandler(
          instanceWrapper,
          requestMethod,
          this.container.getModuleByKey(moduleKey),
          moduleKey,
          methodName,
        )
      : this.createCallbackProxy(
          instance,
          targetCallback,
          methodName,
          moduleKey,
          requestMethod,
        );
    
```
ここでは立ち入りませんが、実際に適用されるメソッドはさらに、いくつかのフィルタが適用されたものになります。

`host`に基づいたフィルタ
```typescript
routeHandler = this.applyHostFilter(host, proxy);
```
バージョンに基づいたフィルタ
```typescript
routeHandler = this.applyVersionFilter(
          router,
          routePathMetadata,
          routeHandler,
        );
```
先述のメタデータ一覧を思い出してもらうと、
- `HOST_METADATA`
- `VERSION_METADATA`
の二つがありました。そう、これらのメタデータはルーティングの文脈においてこの`applyHostFilter`, `applyVersionFilter`で使用されています。

::: note info
 `HOST_METADATA`メタデータは特定のルートのフィルタ条件として適用されるホスト名またはホストパターンを表す
:::

:::note info
 `VERSION_METADATA`メタデータはコントローラーやメソッドに設定されたAPIバージョンを表す
:::

#### ルートパスとハンドラの登録:
最後に、routerMethodRef を使用して、各ルートパスに対してリクエストハンドラをルーターに登録します。

```typescript
routerMethodRef(path, routeHandler);
```
こうして、ルートパスへの各メソッド(Get, Postなど)ごとのリクエストが、対応したルートハンドラ(実際はそのプロキシ)と紐づけられます。
 
# その他メタデータ

ここまでで、`PATH_METADATA`を中心に、`HOST_METADATA` ,`VERSION_METADATA`も含めてメタデータがどのように利用されるかを見てきました。他はおまけのようなものです。ついでなのでさらっと見ていきます。

## `CONTROLLER_WATERMARK`
リポジトリ内で検索してヒットするのは4件のみで、そのうち3件は
- /packages/common/constant.tsでの定義 (上記)
- controller.decorator.tsでの`Reflect.defineMetadata` (前回見たもの)
- ユニットテストファイル (.spec.ts)
なので、実質1箇所のみで使用されています。

```typescript
  /**
   * @param metatype
   * @returns `true` if `metatype` is annotated with the `@Controller()` decorator.
   */
  private isController(metatype: Type<any>): boolean {
    return !!Reflect.getMetadata(CONTROLLER_WATERMARK, metatype);
  }
```

https://github.com/nestjs/nest/blob/e8152d1bbf541a979152285455373801c6d8e692/packages/core/scanner.ts#L737

このprivateメソッドを利用しているのは1箇所"insertModule`というメソッドであり、その中の例外判定の文脈です。

```typescript
  public async insertModule(
    moduleDefinition: any,
    scope: Type<unknown>[],
  ){
    //// 略
    if (
      this.isInjectable(moduleToAdd) ||
      this.isController(moduleToAdd) ||
      this.isExceptionFilter(moduleToAdd)
    ) {
      throw new InvalidClassModuleException(moduleDefinition, scope);
    }
    //// 略
  }
```
ModuleにModuleとして不正なデコレータが付与されている場合に例外とするために使用されており、このことから、`CONTROLLER_WATERMARK` (より一般に`XXXXX_WATERMARK`)というメタデータはデコレータの種別を判定するためのものであることがわかります。

`insertModule`はNestJSのDIで重要な役割を果たしていると思われますが、ここは別の機会で取り上げることとします。

::: note info
`CONTROLLER_WATERMARK`メタデータは、デコレータの種別を表す
:::

## `SCOPE_OPTIONS_METADATA`

こちらについては別の機会に改めて取り上げることとします。結論だけ書いておきましょう。

::: note info
`SCOPE_OPTIONS_METADATA`メタデータは、特定のプロバイダーやコントローラーに適用されるスコープを表す
:::



# 次回
今回は、`@Controller`によって指定されたメタデータがどのようにNestJSのリクエストルーティングで使用されているのかを見ました。次回は、`RoutesResolver`, `RouterExplorer`を掘り下げ、そしてNestJSのルーティングの全体像を明らかにしていくことにします。