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

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

Last updated at Posted at 2024-08-19

前回のまとめなど

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

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

該当するソースコード

@Controllerのおさらい

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

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-metadataReflect.defineMetadataを用いて、

  • CONTROLLER_WATERMARK
  • PATH_METADATA
  • HOST_METADATA
  • SCOPE_OPTIONS_METADATA
  • VERSION_METADATA

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

PATH_METADATA

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

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

本メタデータの利用者(Reflect.getMetadataの実行箇所)として重要なのはRouterExplorer.extractRouterPathで、設定されたメタデータからpathを取得し、返却しています。そして、このextractRouterPathを利用しているのはRoutesResolver.registerRoutersで、これがNestJSにおけるルーティング処理の中心となっているようです。

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 メソッドに基づいて呼び出されます。

    const routerMethodRef = this.routerMethodFactory
          .get(router, requestMethod)
          .bind(router);

プロキシハンドラの作成

createCallbackProxyによりルートハンドラをラップし、実行時コンテキストやエラーハンドリングを含む、「リクエストに対して実際に実行されるメソッド = プロキシ」を作成します。また、ここでスコープに関する条件分岐が入っています。

const proxy = isRequestScoped
      ? this.createRequestScopedHandler(
          instanceWrapper,
          requestMethod,
          this.container.getModuleByKey(moduleKey),
          moduleKey,
          methodName,
        )
      : this.createCallbackProxy(
          instance,
          targetCallback,
          methodName,
          moduleKey,
          requestMethod,
        );
    

ここでは立ち入りませんが、実際に適用されるメソッドはさらに、いくつかのフィルタが適用されたものになります。

hostに基づいたフィルタ

routeHandler = this.applyHostFilter(host, proxy);

バージョンに基づいたフィルタ

routeHandler = this.applyVersionFilter(
          router,
          routePathMetadata,
          routeHandler,
        );

先述のメタデータ一覧を思い出してもらうと、

  • HOST_METADATA
  • VERSION_METADATA
    の二つがありました。そう、これらのメタデータはルーティングの文脈においてこのapplyHostFilter, applyVersionFilterで使用されています。

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

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

ルートパスとハンドラの登録:

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

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箇所のみで使用されています。
  /**
   * @param metatype
   * @returns `true` if `metatype` is annotated with the `@Controller()` decorator.
   */
  private isController(metatype: Type<any>): boolean {
    return !!Reflect.getMetadata(CONTROLLER_WATERMARK, metatype);
  }

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

  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で重要な役割を果たしていると思われますが、ここは別の機会で取り上げることとします。

CONTROLLER_WATERMARKメタデータは、デコレータの種別を表す

SCOPE_OPTIONS_METADATA

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

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

次回

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

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