前回のまとめなど
これまでで、NestJSにおけるデコレータ(@Controllerなど)の内部的な動作と、指定された情報がどのように利用されているかをみました。今回は、前回の記事で触れた二つの重要なクラスであるRoutesResolverとRouterExplorerについて詳しく掘り下げます。
RoutesResolver
RoutesResolverはNestアプリケーションのルーティングの中核をなすクラスです。NestJSアプリケーションを作成する際NestFactory.createによって生成されるNestApplicationインスタンスのコンストラクタ内で、RoutesResolverは生成されます。
export class NestApplication
extends NestApplicationContext<NestApplicationOptions>
implements INestApplication
{
// 省略
constructor(
...
) {
// 省略
this.routesResolver = new RoutesResolver(
this.container,
this.config,
this.injector,
this.graphInspector,
);
}
RoutesResolverはResolverインターフェースを実装しています。
export interface Resolver {
resolve(instance: any, basePath: string): void;
registerNotFoundHandler(): void;
registerExceptionHandler(): void;
}
resolveの実装
public resolve<T extends HttpServer>(
applicationRef: T,
globalPrefix: string,
) {
...
}
ジェネリクスが使われており、resolveの第一引数であるinstance: anyはHttpServerインターフェースを実装することがわかります(Expressのexpress.Routerなど)。
実際の処理を見てみると、現在のNestJSアプリケーションに存在するModuleの全体を取得し、それぞれのコントローラを取得してthis.registerRoutersを実行しています。
const modules = this.container.getModules();
modules.forEach(({ controllers, metatype }, moduleName) => {
const modulePath = this.getModulePathMetadata(metatype);
this.registerRouters(
controllers,
moduleName,
globalPrefix,
modulePath,
applicationRef,
);
});
this.getModulePathMetadataについては省略して、先に進みます。
public registerRouters(
// 対象のModuleに登録されたController群 ("ルートマップ")
routes: Map<string | symbol | Function, InstanceWrapper<Controller>>,
// @Moduleを付与したクラスの名称
moduleName: string,
// アプリケーション全体のパスに対するprefix ('v1'など)
globalPrefix: string,
// getModulePathMetadataでMODULE_PATHメタデータから取得された、
// AppModuleの`@Module`において`RouterModule.register`で指定するモジュールパス
modulePath: string,
// express.Routerなど
applicationRef: HttpServer,
) {
// ルートマップ内の各コントローラをループ処理
routes.forEach(instanceWrapper => {
//省略
// コントローラのルートパスを抽出 (一般には複数の@Getや@Postなどがあるのでパスが複数)
const routerPaths = this.routerExplorer.extractRouterPath(
metatype as Type<any>,
);
// 省略
//各ルートパスについての処理を実行
routerPaths.forEach(path => {
// 省略
// ルートの探索と登録を実行
this.routerExplorer.explore(
instanceWrapper, // 個々のコントローラのインスタンス
moduleName, // コントローラが属するモジュール名
applicationRef, // express.Routerなど(HttpServer)
host, // @Controller(...)で指定されたホスト情報
routePathMetadata,// 関連メタ情報群
);
});
});
}
routePathMetadataについては、modulePathやglobalPrefixなどを含んでおり、各種デコレータで指定された情報をまとめたものになります。インターフェース定義は以下を参照。
RoutesResolverはAppModuleに登録された各モジュールについて、各コントローラの個別のルートパス(@Get()や@Post()などで指定される)単位まで処理を分解することが責務
RouterExplorer
Resolverによって解決されたパス情報をもとに、RouterExplorerはapplicationRef、すなわちexpress.Routerのようなルーティングオブジェクトにパスそのハンドラを登録していきます。これにより、NestJSのデコレータで宣言されたルートが、実際のHTTPリクエストに応じて正しく処理されるようになります。前回で軽く内容を見ましたが、改めて追っていきます。
public explore<T extends HttpServer = any>(
instanceWrapper: InstanceWrapper,
moduleKey: string,
applicationRef: T,
host: string | RegExp | Array<string | RegExp>,
routePathMetadata: RoutePathMetadata,
) {
const { instance } = instanceWrapper;
const routerPaths = this.pathsExplorer.scanForPaths(instance);
this.applyPathsToRouterProxy(
applicationRef,
routerPaths,
instanceWrapper,
moduleKey,
routePathMetadata,
host,
);
}
ここで、this.pathExplorer.scanForPathsというメソッドが登場し、RouteDefinition[]型のroutesPathsを返しています。ここには、パスやメソッド名などの情報、そしてコールバックが含まれます。
export interface RouteDefinition {
path: string[];
requestMethod: RequestMethod;
targetCallback: RouterProxyCallback;
methodName: string;
version?: VersionValue;
}
RouterProxyCallback型は以下で定義されており、expressにおけるハンドラ関数=ルートが一致した時に実行される関数です。
export type RouterProxyCallback = <TRequest, TResponse>(
req?: TRequest,
res?: TResponse,
next?: () => void,
) => void;
前回の記事で見た通り、applyPathsToRouterProxy以降の処理はシンプルで、基本はRouteDefinitionのそれぞれについて、applicationRef = express.Routerにパスとハンドラを登録していくだけです。ということで、あとはこのRouteDefinitionを返すメソッド(を提供するクラスPathsExplorer)を理解すれば十分でしょう。
RouterExplorerは「ルート定義情報」をもとに、コントローラクラスで定義された各ルートハンドラをexpress.Routerのような実際のルーティングオブジェウト(HTTPサーバー)に登録する。
PathsExplorer
PathsExplorer.scanForPathsの実装は以下のようになっています。
export class PathsExplorer {
constructor(private readonly metadataScanner: MetadataScanner) {}
public scanForPaths(
instance: Controller,
prototype?: object,
): RouteDefinition[] {
const instancePrototype = isUndefined(prototype)
? Object.getPrototypeOf(instance)
: prototype;
return this.metadataScanner
.getAllMethodNames(instancePrototype)
.reduce((acc, method) => {
const route = this.exploreMethodMetadata(
instance,
instancePrototype,
method,
);
if (route) {
acc.push(route);
}
return acc;
}, []);
}
ここでMetadataScacnner#getAllMethodNamesはControllerのプロトタイプを受け、そこに定義されているメソッドの一覧を取得します。
取得されたメソッドのそれぞれについて、exploreMethodMetadataでメソッドを取得し、RouteDefinitionを組み立てます。
public exploreMethodMetadata(
instance: Controller, // Controllerクラスのインスタンス
prototype: object, // 上記のプロトタイプ
methodName: string, // Controllerクラスで定義されたあるメソッドの名前
): RouteDefinition | null {
// コールバックメソッドを取得
const instanceCallback = instance[methodName];
// そのメソッドのPATH_METADATAから、@Get(routePath)などで指定したルートパスを取得
const prototypeCallback = prototype[methodName];
const routePath = Reflect.getMetadata(PATH_METADATA, prototypeCallback);
if (isUndefined(routePath)) {
return null;
}
// @Getや@Postなどで指定しMETHOD_METADATAで管理されたHTTPメソッド名を取得
const requestMethod: RequestMethod = Reflect.getMetadata(
METHOD_METADATA,
prototypeCallback,
);
// @Versionデコレータで指定したバージョン値を取得
const version: VersionValue | undefined = Reflect.getMetadata(
VERSION_METADATA,
prototypeCallback,
);
// ルートパスを必要に応じて加工
const path = isString(routePath)
? [addLeadingSlash(routePath)]
: routePath.map((p: string) => addLeadingSlash(p));
// まとめて返却
return {
path,
requestMethod,
targetCallback: instanceCallback,
methodName,
version,
};
}
}
PathsExplorerはコントローラクラスのインスタンスから、ルートパスやハンドラ関数の実体などを取得し、ルート定義情報を生成する。
まとめ
今回の記事では、NestJSのルーティング処理における2つの重要なクラス、RoutesResolverとRouterExplorerについて詳しく見てきました。
まず、RoutesResolverは、アプリケーション内のすべてのモジュールとコントローラを取得し、それらのルート定義を整理する役割を果たします。このクラスは、各コントローラが持つルートパスやHTTPメソッドの情報を集約し、RouterExplorerに渡す準備を整えます。
次に、RouterExplorerは、RoutesResolverから受け取ったルート定義をもとに、実際のルーティング処理を行います。具体的には、express.RouterなどのHTTPサーバーに対して、ルートとそのハンドラを登録します。このプロセスにより、デコレータを通じて宣言されたルートが正しくHTTPサーバーへ反映されるようになります。
①-③でNestJSのルーティング動作については大体見えてきました。次回からは、@Injectableデコレータ関連の実装を起点にして、NestJSのもう一つの重要な要素である依存性注入(DI)の理解を深めることにします。