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①: @Controllerは何をするのか

Last updated at Posted at 2024-08-18

なぜ書いているのか

ちょっくらNestJSを触ってみようと思ったものの、Angular未経験の自分にはデコレータ中心の書き方に馴染みがない & なぜこれで動くのかが理解できずに気持ち悪いので、公式リポジトリのソースコードを読んでみることにした。

前提知識

本記事で扱わないこと

  • NestJSの特徴
  • NestJSの使い方
    など

Controllerのソースコードを読む

公式リポジトリのcontroller.decorator.tsのソースコードは以下にあります。
https://github.com/nestjs/nest/blob/master/packages/common/decorators/core/controller.decorator.ts

後述のControllerOptionsインターフェースの宣言に続き、引数別のデコレータ関数の宣言が続いています。引数として取りうるのは以下の3種類

  • Controller()
  • Controller(prefix: string | string[])
  • Controller(options: ControllerOptions)

公式ドキュメント含め、サンプルコード系でよく使われるのは真ん中でしょう。Controller()の場合は'/'を指定した場合と同値になるようです。

3番目で使用されているControllerOptionsインターフェースは以下のように宣言されています。

/**
 * Interface defining options that can be passed to `@Controller()` decorator
 *
 * @publicApi
 */
export interface ControllerOptions extends ScopeOptions, VersionOptions {
  /**
   * Specifies an optional `route path prefix`.  The prefix is pre-pended to the
   * path specified in any request decorator in the class.
   */
  path?: string | string[];

  /**
   * Specifies an optional HTTP Request host filter.  When configured, methods
   * within the controller will only be routed if the request host matches the
   * specified value.
   *
   * @see [Routing](https://docs.nestjs.com/controllers#routing)
   */
  host?: string | RegExp | Array<string | RegExp>;
}

path?については2番目のパターンと同一の情報であり、違いはもう一つのhostにあり、ホスト名をもとにしたルーティングが可能であることを示しています。コメントに記載のある公式ドキュメントのSub-Domain Routingをみると、例として以下が挙げられています。

@Controller({ host: ':account.example.com' })
export class AccountController {
  @Get()
  getInfo(@HostParam('account') account: string) {
    return account;
  }
}

ここで、ホスト名には動的なパラメータが指定されており、例えばリクエストが user123.example.com というホスト名で送信された場合、account トークンには user123 という値が入ります。そして、コントローラのメソッド引数において@HostParam()デコレーターを使用することで、その値をメソッド内で使用することができます。

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);
  };
}

省略したパラメータの設定 (単に指定されたものがあればそれを、なければundefinedにする、という程度のもの)を除けば、Reflect.defineMetadataというメソッドを実行するクラスデコレータを返却するだけになっています。ここで、Reflect.defineMetadataはreflect-metadataライブラリで提供されており、@Controllerが付与されたクラスに対してメタ情報を付与しています。付与されたメタデータはReflect.getMetadataによって取得可能です。

@Controllerとは何か

ここから、@Controllerは、CONTROLLER_WATERMARKPATH_METADATAといったメタデータを付与するところまでが責務であり、NestJSを理解するにはそれを利用する側(=Reflect.getMetaDataを行っている場所)が本質的なのだろうということがわかります。

これは考えてみれば当たり前で、decoratorはあくまでもクラスやそのメソッドに対して付加的な処理を行うことができる機能です。routing、すなわち「どんな条件で実行されるか」を制御するにはデコレータではなくそのクラスやメソッドを呼び出す側で処理が必要になります。

別の例: @Injectableデコレータの実装

もう一つ、@Injectableのソースコードを見てみましょう。こちらも@Controllerと同様にメタデータの付与が責務だろうと予想されます。
https://github.com/nestjs/nest/blob/master/packages/common/decorators/core/injectable.decorator.ts

export function Injectable(options?: InjectableOptions): ClassDecorator {
  return (target: object) => {
    Reflect.defineMetadata(INJECTABLE_WATERMARK, true, target);
    Reflect.defineMetadata(SCOPE_OPTIONS_METADATA, options, target);
  };
}

予想通り、こちらもINJECTABLE_WATERMARKSCOPE_OPTIONS_METADATAというkeyでメタデータを付与することしかしていません。

別の例: @UseGuards

性質の違うDecoratorとして、@UseGuardsを見てみましょう。公式ドキュメントにある通り、本デコレータはリクエストハンドラの実行条件を設定するものです。以下の例では、GETリクエストに対して、AuthGuardクラスで実装されたcanActivateがtrueを返す場合のみfindAllを実行するように指定しています。

@Controller('users')
export class UsersController {
  
  @Get()
  @UseGuards(AuthGuard)
  findAll() {
    // ユーザー一覧を取得するロジック
  }
}

@Controller@Injectableと異なり、@UseGuardsの責務は一般的な(メソッド)デコレータの処理にかなり近いことがわかります。ソースコードを見てみましょう。

export function UseGuards(
  ...guards: (CanActivate | Function)[]
): MethodDecorator & ClassDecorator {
  return (
    target: any,
    key?: string | symbol,
    descriptor?: TypedPropertyDescriptor<any>,
  ) => {
    const isGuardValid = <T extends Function | Record<string, any>>(guard: T) =>
      guard &&
      (isFunction(guard) ||
        isFunction((guard as Record<string, any>).canActivate));

    if (descriptor) {
      validateEach(
        target.constructor,
        guards,
        isGuardValid,
        '@UseGuards',
        'guard',
      );
      extendArrayMetadata(GUARDS_METADATA, guards, descriptor.value);
      return descriptor;
    }
    validateEach(target, guards, isGuardValid, '@UseGuards', 'guard');
    extendArrayMetadata(GUARDS_METADATA, guards, target);
    return target;
  };
}

色々ありますが、重要なのは以下の3つです。

validateEach(target, guards, isGuardValid, '@UseGuards', 'guard');
extendArrayMetadata(GUARDS_METADATA, guards, target);
return target;

validateEachこちらで定義され、predicate = isGuardValidによって指定されたGuards全てがGuardsとして有効なものかどうかを見ています (@UseGuardsは複数のGuardsを引数に取ることができる)。そして、不正な場合は例外を投げています。

すなわち、正常なGuardsが指定されている場合、@UseGuardsの処理は実質的にextendArrayMetadataのみで、@Controllerと同様にここでもコアになるのはMetadata付与である、ということがわかります。extendArrayMetadataの実装を一応いかに示しますが想像通りでしょう。

export function extendArrayMetadata<T extends Array<unknown>>(
  key: string,
  metadata: T,
  target: Function,
) {
  const previousValue = Reflect.getMetadata(key, target) || [];
  const value = [...previousValue, ...metadata];
  Reflect.defineMetadata(key, value, target);
}

次回

初回である今回の記事では、NestJSの各種デコレータがMetadataの付与を責務としていることを見ました。次回の記事では、こうして付与されたメタデータがどのようにNestJSのリクエストルーティングやDIに使われるかを掘り下げていきます。

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?