프로젝트 전체 코드는 https://github.com/JinhyeokFang/router-with-decorator 에서 확인하실 수 있습니다.
폴더 구조는 다음과 같습니다.
/src
/decorators
get.ts - @Get()이 들어갈 파일입니다.
post.ts - @Post()가 들어갈 파일입니다.
/hello
hello.router.test.ts
hello.router.ts - 데코레이터로 구현한 라우터가 들어갑니다.
root.router.test.ts
root.router.ts - 데코레이터로 구현한 라우터를 Express 라우터로 변환하여
서버 인스턴스에 넣어줍니다.
server.ts - 서버 인스턴스를 생성합니다.
jest.json - 단위 테스트용 설정 파일입니다.
/test
server.e2e-spec.ts
jest-e2e.json - E2E 테스트용 설정 파일입니다.
HelloRouter의 테스트 코드를 구현하고 yarn test
로 실행합니다.
// src/hello/hello.router.test.ts
import { Response } from 'express';
import { HelloRouter } from './hello.router';
describe('HelloRouter', () => {
let helloRouter: HelloRouter;
beforeEach(() => {
helloRouter = new HelloRouter();
});
it('helloGet()', () => {
const mockedSend = jest.fn();
const res: Response = {
send: mockedSend,
} as unknown as Response;
helloRouter.helloPost(null, res);
expect(mockedSend).toBeCalledWith({
success: true,
message: 'Hello World!',
});
});
it('helloPost()', () => {
const mockedSend = jest.fn();
const res: Response = {
send: mockedSend,
} as unknown as Response;
helloRouter.helloPost(null, res);
expect(mockedSend).toBeCalledWith({
success: true,
message: 'Hello World!',
});
});
});
테스트 코드가 실패합니다. 테스트가 통과되도록 HelloRouter를 구현합니다.
// src/hello/hello.router.ts
import { Request, Response } from 'express';
export class HelloRouter {
helloGet(req: Request, res: Response) {
res.send({
success: true,
message: 'Hello World!',
});
}
helloPost(req: Request, res: Response) {
res.send({
success: true,
message: 'Hello World!',
});
}
}
테스트를 통과했으니 HelloRouter를 Express의 Router로 변환할 RootRouter를 구현해보겠습니다.
RootRouter 테스트 코드를 작성합니다.
// src/root.router.test.ts
import { Router } from 'express';
import { HelloRouter } from './hello/hello.router';
import { RootRouter } from './root.router';
describe('RootRouter', () => {
let rootRouter: RootRouter;
beforeEach(() => {
rootRouter = new RootRouter();
});
it('transformToExpressRouter()', () => {
const expressRouter = rootRouter.transformToExpressRouter(HelloRouter);
// 결과 검증용 Router
const helloRouter = new HelloRouter();
const router = Router();
router.get('/hello', helloRouter.helloGet);
router.post('/hello', helloRouter.helloPost);
expect(JSON.stringify(expressRouter.stack)).toStrictEqual(
JSON.stringify(router.stack),
);
});
});
테스트 코드를 통과하도록 RootRouter를 구현합니다
// src/root.router.ts
import { Router } from 'express';
import { HelloRouter } from './hello/hello.router';
type Constructor = new (...args: unknown[]) => unknown;
export class RootRouter {
transformToExpressRouter(router: Constructor) {
const expressRouter = Router();
const routerInstance: HelloRouter = new router() as HelloRouter;
expressRouter.get('/hello', routerInstance.helloGet);
expressRouter.post('/hello', routerInstance.helloPost);
return expressRouter;
}
}
HelloRouter를 Express()
에 넣어 실행할 수 있도록 Server를 구현하겠습니다.
Server E2E 테스트 코드를 작성합니다.
// test/server.e2e-spec.ts
import { Application } from 'express';
import { Server } from '../src/server';
import { RootRouter } from '../src/root.router';
import { HelloRouter } from '../src/hello/hello.router';
import request from 'supertest';
describe('Server E2E Test', () => {
let app: Application;
beforeEach(() => {
const server = new Server();
const rootRouter = new RootRouter();
const helloExpressRouter = rootRouter.transformToExpressRouter(HelloRouter);
server.addRouter(helloExpressRouter);
app = server.app;
});
it('/hello (GET)', () => {
return request(app).get('/hello').expect(200).expect({
success: true,
message: 'Hello World!',
});
});
it('/hello (POST)', () => {
return request(app).post('/hello').expect(200).expect({
success: true,
message: 'Hello World!',
});
});
});
Server를 구현합니다.
// src/server.ts
import Express, { Router } from 'express';
export class Server {
private readonly appInstance = Express();
constructor() {
this.appInstance.use(Express.json());
this.appInstance.use(Express.urlencoded({ extended: true }));
}
addRouter(router: Router) {
this.appInstance.use(router);
}
get app() {
return this.appInstance;
}
}
테스트 코드를 통과했습니다.
현재 RootRouter의 Router 변환 함수는 HelloRouter의 helloGet과 helloPost만 변환할 수 있습니다.
// src/root.router.ts
...
transformToExpressRouter(router: Constructor) {
const expressRouter = Router();
const routerInstance: HelloRouter = new router() as HelloRouter;
expressRouter.get('/hello', routerInstance.helloGet);
expressRouter.post('/hello', routerInstance.helloPost);
return expressRouter;
}
...
HelloRouter가 변경되거나 HelloRouter가 아닌 것을 넣어도 동작하도록 수정할 필요가 있습니다.
이를 위해서는 Router의 함수들이 어떤 메소드(ex: GET, POST, PUT)
를 사용해 어느 경로(ex: /hello)
로 라우팅되는 지 RootRouter가 알 수 있어야 합니다.
메소드와 경로를 메타데이터에 기록하는 @Get()
데코레이터를 구현하겠습니다.
// src/decorators/get.ts
export const Get =
(route: string) =>
(target: any, key: string, descriptor: PropertyDescriptor) => {
Reflect.defineMetadata(
key,
{
method: 'get',
route,
},
target.constructor,
);
};
메타데이터를 사용하도록 RootRouter를 수정합니다.
// src/root.router.ts
import { Router } from 'express';
import { HelloRouter } from './hello/hello.router';
type Constructor = new (...args: unknown[]) => unknown;
export class RootRouter {
transformToExpressRouter(router: Constructor) {
const expressRouter = Router();
const routerInstance: HelloRouter = new router() as HelloRouter;
Object.getOwnPropertyNames(router.prototype)
.map((key: string) => {
return {
route: Reflect.getMetadata(key, router),
key,
}
})
.filter((data) => data.route !== undefined)
.forEach((data) => {
expressRouter[data.route.method](
data.route.route,
routerInstance[data.key],
);
});
expressRouter.post('/hello', routerInstance.helloPost);
return expressRouter;
}
}
HelloRouter에 @Get()
데코레이터를 넣어줍니다.
// src/hello/hello.router.ts
import 'reflect-metadata';
import { Request, Response } from 'express';
import { Get } from '../decorators/get';
export class HelloRouter {
@Get('/hello')
helloGet(req: Request, res: Response) {
res.send({
success: true,
message: 'Hello World!',
});
}
helloPost(req: Request, res: Response) {
res.send({
success: true,
message: 'Hello World!',
});
}
}
@Post()
도 @Get()
와 똑같이 구현하고 테스트 코드를 실행합니다.
정상적으로 동작하는 것을 확인할 수 있습니다.
현재 RootRouter는 Router를 변환하는 역할만 수행하고 있습니다.
// test/server.e2e-spec.ts
...
describe('Server E2E Test', () => {
let app: Application;
beforeEach(() => {
const server = new Server();
const rootRouter = new RootRouter();
const helloExpressRouter = rootRouter.transformToExpressRouter(HelloRouter);
server.addRouter(helloExpressRouter);
app = server.app;
});
...
RootRouter를 Server에 집어넣을 수 있도록 코드를 수정하겠습니다.
E2E 테스트 코드를 수정합니다.
// test/server.e2e-spec.ts
...
describe('Server E2E Test', () => {
let app: Application;
beforeEach(() => {
const server = new Server();
const rootRouter = new RootRouter();
rootRouter.addRouter(HelloRouter);
server.addRouter(rootRouter.router);
app = server.app;
});
...
테스트 코드가 성공하도록 RootRouter를 수정합니다.
// src/root.router.ts
import { Router } from 'express';
type Constructor = new (...args: unknown[]) => unknown;
export class RootRouter {
private readonly routerInstance = Router();
addRouter(router: Constructor) {
const expressRouter = this.transformToExpressRouter(router);
this.routerInstance.use(expressRouter);
}
transformToExpressRouter(router: Constructor) {
const expressRouter = Router();
const routerInstance = new router();
Object.getOwnPropertyNames(router.prototype)
.map((key: string) => {
return {
route: Reflect.getMetadata(key, router),
key,
};
})
.filter((data) => data.route !== undefined)
.forEach((data) => {
expressRouter[data.route.method](
data.route.route,
routerInstance[data.key],
);
});
return expressRouter;
}
get router() {
return this.routerInstance;
}
}