なぜ書いているのか
ちょっくらNestJSを触ってみようと思ったものの、Angular未経験の自分にはデコレータ中心の書き方に馴染みがない & なぜこれで動くのかが理解できずに気持ち悪いので、公式リポジトリのソースコードを読んでみることにした。
前提知識
- Typescript
- Decoratorの概念と基礎 (https://zenn.dev/monicle/articles/b6a409eeb62f41)
- NestJS公式ドキュメントのOverView程度 (https://docs.nestjs.com/first-steps)
本記事で扱わないこと
- 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_WATERMARK
やPATH_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_WATERMARK
とSCOPE_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に使われるかを掘り下げていきます。