nest.js

[고스락 티켓 2.0] nest js swagger 같은 코드 여러 응답 예시 만들기 (3) - 에러응답 데코레이터 만들기

ImNM 2022. 8. 16. 14:34

 

 

[고스락 티켓 2.0] nestjs swagger 같은 코드 여러 응답 예시 만들기 (1) - @ApiProperty로 객체 만들기

스웨거에서 같은 코드의 응답은 예시를 여러개를 넣지를 못한다. 위처럼 기술한경우 하나만 적힌다. 또한 응답 예시 (Example Value ) 를 어느 경우엔 어떤 응답이 온다고 알려주고 싶으면 content 부

devnm.tistory.com

 

 

[고스락 티켓 2.0] nestjs swagger 같은 코드 여러 응답 예시 만들기 (2) - 성공응답 데코레이터 만들기

[고스락 티켓 2.0] nestjs swagger 같은 코드 여러 응답 예시 만들기 (1) - @ApiProperty로 객체 만들기 스웨거에서 같은 코드의 응답은 예시를 여러개를 넣지를 못한다. 위처럼 기술한경우 하나만 적힌다.

devnm.tistory.com

 

이전 포스팅들이다 서론은 생략하고 바로 들어가겠숩니다!

 

이글을 통해서 얻을 수 있는 점들

  • 같은 코드에 여러 성공응답 예시를 작성하는 방법
  • 공통 에러응답과 , 에러내용까지 응답예시로 보여주는 방법

들어가기 전에

 

[고스락 티켓 2.0] nest js ValidationError custom

nest js 에서는 검증도중에 오류가 발생하면 400번 ValidationError를 발생시키는데 { "statusCode": 400, "error": "Bad Request", "message": ["email must be an email"] } 이런 형식으로 나온다. 사실뭐 그냥..

devnm.tistory.com

고티켓 프로젝트에서는 검증오류를 커스텀해서 쓰고 있으니 참고하시길 바란다.!

 

 

에러응답 dto


import { EnumToArray } from '../utils/enumNumberToArray';

export class ErrorCommonResponse<T> {
  @ApiProperty({ enum: EnumToArray(HttpStatus), description: '상태코드' })
  @Expose()
  readonly statusCode: number;

  @ApiProperty({ type: String, description: '에러 발생시간' })
  @Expose()
  readonly timestamp: Date;

  @ApiProperty({ type: String, description: '에러 발생 url' })
  @Expose()
  readonly path: string;

  @ApiProperty({ type: String, description: '에러 발생 메소드' })
  @Expose()
  readonly method: string;

  @ApiProperty({
    type: 'generic',
    description:
      'HttpExceptionErrorResponseDto,ValidationErrorResponseDto 두가지가 올수있습니다.'
  })
  @Expose()
  error: T;
}

src/common/errors/ErrorCommonResponse.dto.ts

 

공통응답 디티오이다. error 안에 HttpExceptionErrorResponseDto , ValidationErrorResponseDto 두개가 올수있고

HttpExceptionErrorResponseDto 는 HttpException을 extends 한 에러들의 응답 디티오이다.

 

error 는 제네릭 형태이지만 제네릭이 컴파일 타임에 타입만 도와주는거라 실제로 타입정보를 얻어올수가없다.

따라서 type을 그냥 문자열 generic으로 적고 makeInstanceByApiProperty 함수에서 별도로 처리를 한다.

 

export class HttpExceptionErrorResponseDto {
  @ApiProperty({
    enum: HttpErrorNameEnum,
    description: '에러명'
  })
  @Expose()
  error: string;

  @ApiProperty({
    type: String,
    description: '에러메시지'
  })
  @Expose()
  message: string;

  @ApiProperty({
    enum: EnumToArray(HttpStatus),
    description: '상태코드 400~500번대만 봐주세용'
  })
  @Expose()
  statusCode: number;

  @ApiProperty({
    type: String,
    description: '에러코드가 넘어옵니다. 널값일 수 있습니다!!!',
    nullable: true
  })
  @Expose()
  code?: string;

  constructor(
    statusCode: number,
    error: string,
    message: string,
    code?: string
  ) {
    this.error = error;
    this.statusCode = statusCode;
    this.message = message;
    this.code = code;
  }
}

srv/common/errors/HttpExceptionError.response.dto.ts

HttpExceptionErrorResponseDto 는 HttpException을 extends 한 에러들의 응답 디티오이다.

code를 통해서 에러 코드화를 시킨 부분도 있다.

 

export class ValidationErrorResponseDto {
  @ApiProperty({
    type: String,
    description: '에러명',
    example: 'ValidationError'
  })
  @Expose()
  error = 'ValidationError';

  @ApiProperty({
    type: String,
    description: '밸리데이션 에러는 코드도 ValidationError입니다.',
    example: 'ValidationError'
  })
  @Expose()
  code = 'ValidationError';

  @ApiProperty({
    type: String,
    description: '에러메시지',
    example: '검증오류'
  })
  @Expose()
  message = '검증오류';

  @ApiProperty({
    type: Number,
    description: '400 검증오류 고정',
    example: 400
  })
  @Expose()
  statusCode = 400;

  @ApiProperty({
    // type: { fieldName: ['errorinfoOfString'] },
    description: '필드 : [에러정보] 형식의 에러정보가 담긴 객체입니다.',
    example: { fieldName: ['errorinfoOfString'] }
  })
  @Expose()
  validationErrorInfo: Record<string, Array<string>>;

  constructor(validationErrorInfo: Record<string, Array<string>>) {
    this.validationErrorInfo = validationErrorInfo;
  }
}

srv/common/errors/ValidationError.response.dto.ts

검증오류 커스텀 에러 응답 디티오이다. validationErrorInfo 에 필드 : ["에러정보", "에러정보2"] 이런형식의 object가 들어간다.

 

 

ErrorResponse.decorator.ts


export interface ErrorResponseOption {
  /**
   * HttpException을 extend한 에러 타입을 인자로 받습니다.
   * 예시 : BadRequestException
   */
  model: Type<HttpException>;
  /**
   * 예시의 제목을 적습니다
   */
  exampleTitle: string;
  /**
   * 서비스 레이어에서 적었던 오류 메시지를 기술합니다.
   */
  message: string | Record<string, Array<string>>;
  /**
   * 어떠한 상황일 때 오류가나는지 기술합니다.
   */
  exampleDescription: string;
  /**
   * 에러 코드에 대해 기술합니다.
   */
  code?: string;
}

model을 통해서 401 ,400 , 403 등 HttpException 을 extends 한 에러를 인자로 받는다

표시될 에러의 제목을 적고

message를 통해서 에러메시지를 적는다

code 부분은 에러코드를 기술해서 클라이언트단에서 에러메시지를 좀 커스텀하고싶으면 이부분을 통해서 하면된다.

 

 

export const ErrorResponse = (
  StatusCode: HttpStatus,
  errorResponseOptions: ErrorResponseOption[]
) => {
  let flagValidationErrorExist = false;
  const examples = errorResponseOptions
    .map((error: ErrorResponseOption) => {
      let innerErrorDto;
      if (error.model === CustomValidationError) {
        flagValidationErrorExist = true;
        if (typeof error.message === 'string') {
          throw Error(
            '검증오류는 넘겨줄때 Record<string, Array<string>> 타입으로 주셔야합니다.'
          );
        }
        innerErrorDto = new ValidationErrorResponseDto(error.message);
      } else {
        if (typeof error.message !== 'string') {
          throw Error('http오류는 넘겨줄때 string 타입으로 주셔야합니다.');
        }
        innerErrorDto = new HttpExceptionErrorResponseDto(
          StatusCode,
          error.model.name,
          error.message,
          error.code
        );
      }
      const commonErrorInstance =
        makeInstanceByApiProperty<ErrorCommonResponse<any>>(
          ErrorCommonResponse
        );
      commonErrorInstance.error = innerErrorDto;
      return {
        [error.exampleTitle]: {
          value: commonErrorInstance,
          description: error.exampleDescription
        }
      };
    })
    .reduce(function (result, item) {
      Object.assign(result, item);
      return result;
    }, {}); // null 값 있을경우 필터링
  //console.log(examples);
  return applyDecorators(
    ApiExtraModels(
      ErrorCommonResponse,
      HttpExceptionErrorResponseDto,
      ValidationErrorResponseDto
    ),
    ApiResponse({
      status: StatusCode,
      content: {
        'application/json': {
          schema: {
            additionalProperties: { $ref: getSchemaPath(ErrorCommonResponse) },
            oneOf: flagValidationErrorExist
              ? [
                  { $ref: getSchemaPath(ValidationErrorResponseDto) },
                  { $ref: getSchemaPath(HttpExceptionErrorResponseDto) }
                ]
              : [{ $ref: getSchemaPath(HttpExceptionErrorResponseDto) }]
          },
          examples: examples
        }
      }
    })
  );
};

전편이랑 비슷한이야기다!

밸리데이션 에러이면 벨리데이션 에러까지표현해주고, 없으면 HttpException 까지 포현해준다.

2편에서도 성공응답데코레이터에서 사진하나하나 찍어가며 설명해놓았으니 참고하기 바란다!

 


이로써 1편,2편,3편을통해

@ApiProperty 로적은 정보들을 메타데이터를 활용해서 가져와서

예시값을 가진 객체를 생성해서 스웨거에 같은 코드에 여러 응답을 나타내줄 수있는 기능을 만들었다!

  'Auth-1000': {
    model: UnauthorizedException,
    exampleDescription: '헤더 Bearer 형식을 지키지 않고 잘못 요청 보냈을 때',
    exampleTitle: '어세스토큰-잘못된 헤더 요청',
    message: '잘못된 헤더 요청',
    code: 'Auth-1000'
  },

  'Auth-1001': {
    model: UnauthorizedException,
    exampleDescription:
      '손상되거나 Bearer 형식은 맞췄는데 토큰이 이상한 토큰일 때',
    exampleTitle: '어세스토큰-잘못된 토큰',
    message: '잘못된 토큰',
    code: 'Auth-1001'
  },

  'Auth-1002': {
    model: UnauthorizedException,
    exampleDescription: '기한이 지난 토큰일때',
    exampleTitle: '어세스토큰-기한만료',
    message: '기한만료',
    code: 'Auth-1002'
  },
  'Auth-1003': {
    model: UnauthorizedException,
    exampleDescription:
      '어세스토큰은 살아있지만 db에서 유저가 삭제되었을때 (테스트할때 발생할수있는 오류)',
    exampleTitle: '어세스토큰-유저없음',
    message: '없는 유저입니다.',
    code: 'Auth-1003'
  },

src/auth/Errors/AuthErrorDefine.ts

요번엔 위처럼 주요 에러들을 코드화해서 나타냈는데

실제로 프론트단에서 코드로 에러잡어서 메시지를 커스텀하던가 공통으로 에러관리를 하고있다.

 

그람이만!