기존 Express API에 routing-controllers를 선택한 이유
목차
비교하게 된 이유 #
이미 서버 구성, middleware와 controller가 있는 Express API에 type-safe routing과 OpenAPI 문서를 추가하려고 했습니다. 이때 비교한 선택지는 routing-controllers와 tsoa였습니다.
Nest.js는 이번 비교에서 제외했습니다. 기존 Express 애플리케이션을 유지하려는 상황에서 더 큰 application framework로 옮기는 선택은 범위가 달랐기 때문입니다.
두 라이브러리의 차이 #
두 라이브러리 모두 TypeScript decorator를 사용하지만, route를 연결하는 위치가 다릅니다.
| routing-controllers | tsoa | |
|---|---|---|
| Route 등록 | decorator가 붙은 controller class를 runtime에 등록 | decorator가 붙은 controller에서 route code 생성 |
| 기존 Express 앱 | useExpressServer로 기존 app에 연결 |
생성된 route를 RegisterRoutes로 등록 |
| Middleware | 기존 Express middleware와 class middleware를 함께 사용 | 생성된 route와 tsoa runtime 흐름에 맞춰 구성 |
| OpenAPI | routing-controllers-openapi 같은 integration 사용 |
tsoa CLI로 spec 생성 |
| 이 프로젝트에서의 설정 비용 | decorator와 routing-controllers 옵션 추가 | tsoa config, 생성 명령과 생성 결과 관리 추가 |
어느 라이브러리가 항상 더 낫다고 판단한 것은 아닙니다. 이미 만들어진 application을 얼마나 바꿔야 하는지를 기준으로 선택했습니다.
Routing과 middleware #
routing-controllers는 기존 Express application과 controller를 가까운 경계에서 연결합니다. 이 프로젝트에서는 이미 생성한 app을 useExpressServer에 전달하고, route prefix와 controller, middleware, interceptor, authorization checker를 함께 설정했습니다.
useExpressServer(app, {
routePrefix: '/api',
controllers: [/* controller paths */],
middlewares: [HttpErrorHandler, LoggingHandler],
interceptors: [/* interceptor paths */],
authorizationChecker
});
이 구조에서는 필요한 곳에서 Request와 Response를 계속 사용할 수 있었습니다. route 선언 방식을 바꾼다는 이유만으로 middleware까지 다시 작성할 필요도 없었습니다.
반면 tsoa는 route 생성 단계가 필요합니다. 보통 TypeScript compile 전이나 development 과정에서 CLI를 실행합니다.
{
"scripts": {
"build": "tsoa spec-and-routes -c=tsoa.production.json && tsc -p ./",
"dev": "nodemon -x tsoa spec-and-routes -c=tsoa.json"
}
}
생성된 route file을 기준으로 운영하려는 project라면 자연스러운 방식입니다. 다만 기존 Express server에서는 별도의 결과물과 실행 단계를 함께 관리해야 했습니다.
Documentation #
두 선택지 모두 OpenAPI 문서를 만들 수 있지만, 연결하는 방식은 달랐습니다.
routing-controllers에서는 controller와 DTO에 이미 쌓인 metadata를 활용하기 위해 routing-controllers-openapi와 class-validator-jsonschema를 사용했습니다.
const spec = routingControllersToSpec(
getMetadataArgsStorage(),
routingControllerOptions,
additionalProperties
);
tsoa는 config file을 기준으로 spec과 route file을 생성합니다. entry file, controller 경로, output directory, base path와 compiler option 등을 설정해야 합니다.
{
"entryFile": "src/server.ts",
"controllerPathGlobs": ["src/**/*.controller.ts"],
"spec": {
"outputDirectory": "src",
"basePath": "/api",
"specVersion": 3
},
"routes": {
"routesDir": "src",
"basePath": "/api"
}
}
설정이 끝난 뒤에는 생성 방식이 편리합니다. 다만 이 project에서는 이미 route와 문서화 설정이 있었기 때문에, 기존 구조에 바로 연결할 수 있는 쪽이 더 적합했습니다.
내가 선택한 방식 #
이 project에서는 기존 Express 경계를 유지할 수 있다는 이유로 routing-controllers를 선택했습니다. route를 decorator 기반 controller로 옮기면서도 middleware 흐름은 유지했고, 같은 controller metadata로 OpenAPI 문서도 만들 수 있었습니다.
그렇다고 tsoa가 나쁜 선택이라는 뜻은 아닙니다. 생성된 route file과 명확한 compile-time 생성 단계를 중심으로 project를 구성한다면 판단은 달라질 수 있습니다.