前回のまとめなど
これまでで、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)の理解を深めることにします。