NestJS에서 컨트롤러를 정의할 때에는, 컨트롤러 메서드에 @Controller
데코레이터로 원하는 route path를 등록한다.
그리고 내부의 각 메서드에서 원하는 HTTP Method 및 하위 경로를 지정하여, 각 경로에 메서드 핸들러를 등록한다.
import { Controller, Get } from "@nestjs/common";
@Controller("user")
export class UserController {
// GET /user
@Get()
findAll(): string {
return "return all users";
}
// POST /user/create
@Post("create")
create(): string {
return "create new user";
}
}
해당 컨트롤러는 모듈의 controllers 내에 추가되어, 특정 모듈의 컨트롤러로 명시된다.
import { Module } from "@nestjs/common";
import { UserController } from "./user.controller";
@Module({
controllers: [UserController],
})
export class UserModule {}
Nest 앱이 bootstrap 되는 시점에 해당 모듈이 탐색되고, 모듈에 등록된 컨트롤러가 인스턴스화 되어 라우팅이 등록된다.
@Controller
, @Get
/ @Post
등의 데코리이터의 역할NestJS에서 지원하는 데코레이터의 가장 주요한 역할은 특정 클래스/메서드에 metadata를 설정하는 것이다.
이는 @Controller
도 마찬가지이며, 데코레이터가 전달 받은 route 경로를 대상 클래스의 path
metadata로 지정한다.
function Controller(prefix?: string | string[]): ClassDecorator {
// ... prefix에 기본값 등을 설정
return (target: Function) => {
Reflect.defineMetadata(PATH_METADATA, path, target);
};
}
@Get
, @Post
등의 HTTP Method 데코레이터도 동일하게 전달받은 경로를 path
, HTTP Method를 method
metadata에 저장한다.
import { SetMetadata } from "@nestjs/common";
export const Get = (path?: string): MethodDecorator => {
return (target, key, descriptor) => {
Reflect.defineMetadata(PATH_METADATA, path, descriptor.value);
Reflect.defineMetadata(
METHOD_METADATA,
RequestMethod.GET,
descriptor.value
);
};
};
NestApp은 init() 되는 시점에 RoutesResolver를 통해 어플리케이션에 등록된 모든 컨트롤러들을 route path에 매칭하여 등록한다.
export class NestApplication {
private readonly routesResolver: RoutesResolver;
public async init() {
await this.registerRouter();
}
public async registerRouter() {
this.routesResolver.resolve(this.httpAdapter, basePath);
}
}
이 때 RoutesResolver는 모든 모듈을 순회하면서, 각 모듈에 대해 라우팅을 등록하는 메서드를 호출한다.
export class RoutesResolver {
constructor(
private readonly routerProxy: RouterProxy,
private readonly routerExplorer: RouterExplorer
) {}
public resolve() {
const modules = this.container.getModules();
modules.forEach(({ controllers, metatype }, moduleName) => {
this.registerRouters(
controllers,
moduleName,
globalPrefix,
modulePath,
applicationRef,
);
});
}
}
path
메타데이터를 기반으로 라우팅 등록각 모듈에 대해서 호출하는 registerRouters()
메서드를 자세히 살펴보자.
registerRouters()
에서는 먼저 routerExplorer.extractRouterPath()
메서드를 통해 컨트롤러에 등록된 path
metadata를 추출해서 받아온다.
@Controller
의 path에는 string
또는 string[]
로 route path를 지정할 수 있는데, extractRouterPath()
는 해당 값을 바탕으로 일관된 형태의 데이터 구조로 route path 목록을 가져온다.
@Controller('search') -> ['/search']
@Controller(['search', '/create']) -> ['/search', 'create']
이제 해당 route path를 모두 순회하면서 routerExplorer.explore()
를 호출한다.
해당 메서드는 컨트롤러의 모든 메서드들을 탐색하여 route path에 등록하는 역할을 한다.
public registerRouters(
routes: Map<string | symbol | Function, InstanceWrapper<Controller>>,
moduleName: string,
globalPrefix: string,
modulePath: string,
applicationRef: HttpServer,
) {
routes.forEach(instanceWrapper => {
const { metatype } = instanceWrapper;
const routerPaths = this.routerExplorer.extractRouterPath(
metatype as Type<any>,
);
routerPaths.forEach(path => {
const routePathMetadata: RoutePathMetadata = {
ctrlPath: path,
modulePath,
globalPrefix,
controllerVersion,
versioningOptions,
};
this.routerExplorer.explore(
instanceWrapper,
moduleName,
applicationRef,
host,
routePathMetadata,
);
});
});
}
RouterExplorer.explore
메서드는 컨트롤러의 메서드를 모두 순회하면서, 실질적으로 각 route에 handler를 등록한다.
PathsExplorer.scanForPaths
메서드에서 클래스의 메서드들을 모두 순회하여 라우팅 매핑 정보를 파싱하고, applyPathsToRouterProxy
메서드에서 매핑 정보를 이용하여 각 route에 handler를 등록한다.
applyPathsToRouterProxy
는 핸들러와 함께 필요한 미들웨어, 파이프라인, 예외 필터 등을 등록하여 전처리 및 후처리가 될 수 있게 한다.
export class RouterExplorer {
...
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,
);
}
}
PathsExplorer.scanForPaths
에서 최종적으로 컨트롤러의 모든 메서드를 추출해서, 각 메서드의 프로토타입을 바탕으로 라우팅 매핑 정보를 반환하는 것을 확인할 수 있다.
exploreMethodMetadata()
메서드가 각 컨트롤러 메서드의 path
, method
metadata를 기반으로 등록할 하위 route path와 HTTP Method를 추출한다.
export class PathsExplorer {
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;
}, []);
}
public exploreMethodMetadata(
instance: Controller,
prototype: object,
methodName: string
): RouteDefinition | null {
const instanceCallback = instance[methodName];
const prototypeCallback = prototype[methodName];
const routePath = Reflect.getMetadata(PATH_METADATA, prototypeCallback); // path metadata
if (isUndefined(routePath)) {
return null;
}
const requestMethod: RequestMethod = Reflect.getMetadata(
METHOD_METADATA,
prototypeCallback
); // method metadata
return {
path,
requestMethod,
targetCallback: instanceCallback,
methodName,
version,
};
}
}
NestJS에서 라우팅 매핑 정보를 바탕으로, 각 route에 handler를 등록하는 플로우를 정리하면 다음과 같다.
NestApplication.init()
-> RoutesResolver.resolve()
호출RoutesResolver.resolve()
에서 각 모듈을 순회하면서 this.registerRouters()
를 호출RoutesResolver.registerRouters()
에서 모듈 내의 컨트롤러들을 순회하면서 각 컨트롤러의 path
metadata 추출, 각 path에 대해서 컨트롤러의 메서드를 핸들러로 등록하도록 routerExplorer.explore()
를 호출RouterExplorer.explore()
에서 컨트롤러의 모든 메서드를 탐색하기 위해 pathsExplorer.scanForPaths()
호출PathsExplorer.scanForPaths()
에서 컨트롤러의 모든 메서드를 순회하며 this.exploreMethodMetadata()
를 호출하여 각 메서드의 라우팅 매핑 정보 획득PathsExplorer.exploreMethodMetadata()
에서 메서드에 등록된 path
, method
metadata를 바탕으로 라우팅 매핑 정보 추출@Get
등의 데코레이터가 등록되지 않은 메서드) null 반환PathsExplorer.scanForPaths()
로부터 반환 받은 route 매핑 정보를 바탕으로, this.applyPathsToRouterProxy()
를 호출하여 각 route path에 handler 등록path
메타데이터, 각 메서드들의 path
및 method
메타데이터, globalPrefix
등 모든 데이터를 반영하여 route path를 등록