Nestjs의 중복 API Endpoint 방지하기

Nest.js에서 Express 사용 시 발생할 수 있는 중복 API Endpoint 문제를 해결하기 위해, API 경로를 검증하는 모듈을 구현하고 활용하는 방법을 소개합니다.

Contents


Problems

Typescript Decorator 기능을 사용하면서, NestJS 를 사용하다 보면 생각치 못한 문제점들이 발생하는 경우가 종종 있다. 문제는 아주 간단한 방법으로 재현되는데, 다음과 같은 코드를 살펴보자.

import { Body, Controller, Post } from '@nestjs/common';
import { ApiOperation, ApiTags } from '@nestjs/swagger';
import { HelloDto } from '../model/hello.dto';

@ApiTags('hello')
@Controller('hello')
export class HelloWorldController {
  @ApiOperation({
    summary: 'hello',
  })
  @Post('http')
  async httpTest(@Body() dto: HelloDto) {
    return 'hello';
  }
}

@nestjs/swagger 가 정상적으로 세팅된 경우, 당연히(?) 도 스웨거 문서가 잘 세팅되어야 한다.


이제, 위의 코드에 간단하게 다음과 같은 데코레이터를 추가해 보자.

export function ConsoleLog(_, __, descriptor) {
  const original = descriptor.value;

  descriptor.value = function (this: any, ...args: any[]) {
    console.log('arguments', args);
    const result = original.apply(this, args);
    console.log('result', result);

    return result;
  };
}

@ApiTags('hello')
@Controller('hello')
export class HelloWorldController {
  @ApiOperation({
    summary: 'hello',
  })
  @Post('http')
  @ConsoleLog
  async httpTest(@Body() dto: HelloDto) {
    return 'hello';
  }
}


간단하게, 파라미터와 리턴 결과를 console.log 로 출력해주는 함수이다. 위의 함수를 실행시켜 보면, 원하는 기능이 잘 동작함을 확인할 수 있다. 그런데 Swagger 문서를 확인해 보면,


위의 사진과 같이 @Body 정보가 삭제되었음을 알 수 있다.



Solutions

처음에는 descriptor.value 를 덮어씌우는 과정에서 metadata 가 삭제되었다고 생각해서, Reflector 를 사용해서 metadata 를 복사하기도 하고, descriptor 의 prototype 을 원래 함수와 연결해 보기도 하였다. 하지만 그래도 Swagger 문서는 정상적으로 표시되지 않았다. 오랫동안 삽질하다, 결국 Nestjs 의 구현체를 찾아보는 것밖에 답이 없다는 생각이 들어서, 소스코드를 살펴보았다.

@nestjs/swagger 소스코드 를 살펴보면, method.name 필드 값을 참조해서 Metadata 를 읽어들이고 있다. 내가 작성한 소스코드의 descriptor.value 는 익명함수이므로, 클래스의 메서드와 달리 name 필드가 존재하지 않는다. 따라서 다음과 같이 name 값을 수동으로 세팅해 주어야 정상적으로 Swagger 문서가 동작한다.

export function ConsoleLog(_, propertyKey, descriptor) {
  const original = descriptor.value;

  descriptor.value = function (this: any, ...args: any[]) {
    console.log('arguments', args);
    const result = original.apply(this, args);
    console.log('result', result);

    return result;
  };

  Object.defineProperty(descriptor.value, 'name', {
    value: propertyKey.toString(),
    writable: false,
  });
}

@ApiTags('hello')
@Controller('hello')
export class HelloWorldController {
  @ApiOperation({
    summary: 'hello',
  })
  @Post('http')
  @ConsoleLog
  async httpTest(@Body() dto: HelloDto) {
    return 'hello';
  }
}


구글링을 통해 알게 된 또다른 방법도 있다. [Issue] Custom method decorator 를 살펴보면, Javascript 의 Proxy 를 통해 위의 문제를 간단히 해결할 수 있다.

export function ConsoleLog(_, propertyKey, descriptor) {
  const original = descriptor.value;

  descriptor.value = new Proxy(original, {
    apply: function (target, thisArg, args) {
      console.log('arguments', args);
      const result = target.apply(thisArg, args);
      console.log(`result`, result);

      return result;
    },
  });
}

@ApiTags('hello')
@Controller('hello')
export class HelloWorldController {
  @ApiOperation({
    summary: 'hello',
  })
  @Post('http')
  @ConsoleLog
  async httpTest(@Body() dto: HelloDto) {
    return 'hello';
  }
}


Proxy 를 통해 함수 호출 부분만 교체하고, 나머지 property 는 기존 method 의 성질을 그대로 이용할 수 있다.



[@toss/nestjs-aop] fix: enable nestjs swagger [nestjs/swagger] Custom method decorator


이것도 읽어보세요