nest.js

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

ImNM 2022. 8. 13. 13:04

nest js 에서는 검증도중에 오류가 발생하면 400번 ValidationError를 발생시키는데

{
  "statusCode": 400,
  "error": "Bad Request",
  "message": ["email must be an email"]
}

이런 형식으로 나온다.

  사실뭐 그냥 써도 상관없지만 ( 보통 클라단에서 다막기 때문 ) 

클라이언트가 보낸 필드중에 어느부분이 오류인지 알려면 

검증 오류가 난 필드가 무엇이고, 그필드에 해당하는 오류내용이 무엇인지 아래와같은 형식으로 기술을 할려고 한다.

 

만들 형식

 ( code 부분은 다른 HttpExcepiton 을 상속받은 에러들에서 에러코드를 나타내기 위해 만든것인데 , 공통으로 맞춰줄려고 ValidationError 라고 적었다 )

 

이글을 통해서 얻어갈 수 있는 것들

  • useGlobalPipes 에서 exceptionFactory를 통해서 커스텀에러형식을 반환하는 방법
  • CustomValidationError 정의내리기
  • 글로벌 exception필터에서 CustomValidationError를 잡아서 응답 반환하기

 

이글의 예시들은 고스락 티켓 예매 프로젝트 22nd 에서 사용중인 코드들이다.

 

[고스락 티켓 2.0] 두번째 프로젝트는 어떻게 달라졌을까요?

우선 고스락 티켓 예매 프로젝트의 목적은 기존 종이티켓으로 표를 팔러다니던 OB시절에서 정산의 어려움이나 공연 홍보의 어려움 또한 비대면 시대로 돌입하면서 학교에 모이는 인원이 적다보

devnm.tistory.com

 프로젝트에 관련한 내용은 위글과 , 깃허브의 backend-22nd 레포지토리를 참고바란다.

 

useGlobalPipes 에서 exceptionFactory를 통해서 커스텀에러형식을 반환하는 방법


고스락 티켓프로젝트(이하 고티켓) 은 main.ts app 설정 부분에서 글로벌로 인터셉터, 밸리데이션 파이브 등을 등록하여 사용중이다.

  app.useGlobalPipes(
    new ValidationPipe({
      transform: true,
      transformOptions: { enableImplicitConversion: true },
      exceptionFactory: (validationErrors: ValidationError[] = []) => {
        return new CustomValidationError(validationErrors);
      }
    })
  );

main.ts

 

위와 같은 형태로 사용중인데 , enableImplicitConversion 을 이용하면 스트링으로들어온값도 숫자로 바꿀수 있고 그렇다.

중요한점은 class-validation 과 class-transformer의 실행 순서에 관한 부분인데 궁금하시다면

 

[티키타카] nestjs - mongoDB 적용하기

디프만에서 진행한 프로젝트로 nest js 에 mongoDB를 적용하면서 고민한점에 대해서 이야기할려고한다. 우선 얻어갈수있는 부분에 대한 목차이다. 모델설정 , 다른 모델 populate 처리시 타입은? populat

devnm.tistory.com

위글 중반부의 class-validation 과 class-transformer의 실행 시점에 관한 이야기를 보고오시면 될것같다.

 

커스텀으로 정의 내리기위해선 exceptionFactory를 통하면 되는데

ValidationError[] 형으로 인자를받고 일단 CustomValidaitonError의 생성자로 넘겨주었다.

ValidationError 타입

우리는 저 부분에서 constraints 부분을 이용할 것이다.

  @MinLength(11)
  @Matches(/010[0-9]*$/, {
    message: '010으로 시작하는 숫자만 들어와야 합니다.'
  })
  phoneNumber : string

이런식의 에러인데 만약 두 검증 모두 에러가 발생한다면

{
  matches: '010으로 시작하는 숫자만 들어와야 합니다.',
  minLength: 'phoneNumber must be longer than or equal to 11 characters'
}

constraints는 위와같은 형식을 띈다.

 우린 저기에서 value 값만 떼와서 어떤 필드 : ["에러내용" , "에러내용2"] 형식으로 만들어 줄것이다.

 

 

CustomValidationError 정의내리기


import { HttpException, HttpStatus, ValidationError } from '@nestjs/common';

export class CustomValidationError extends HttpException {
  name = 'ValidationError';
  constructor(valdationErrorArray: ValidationError[]) {
    const objectsOfError = valdationErrorArray
      .map((error: ValidationError) => {
        const constrains = error.constraints;
        if (!constrains) return null;
        const constrainsErrorStrings = Object.keys(constrains).map(
          key => constrains[key]
        );
        return { [error.property]: constrainsErrorStrings };
      })
      .filter(e => e) // null 값 있을경우 필터링
      // reduce이용해서 함수형으로 머지하는용도 ( 뭐 유명한소스입니다 스택에 있어요 ㅋ )
      // 깊은 복사는 아닙니다...
      .reduce(function (result, item) {
        if (!item) return result;
        if (!result) return result;
        Object.assign(result, item);
        return result;
      }, {});  
    super(
      {
        error: 'ValidationError',
        message: '검증 오류',
        validationErrorInfo: objectsOfError,
        statusCode: HttpStatus.BAD_REQUEST,
        code: 'ValidationError'
      },
      HttpStatus.BAD_REQUEST
    );
  }
}

src/common/error/ValidationError.ts

 

현재 고티켓에서 사용하고 있는 CustomValidationError 이다. 

error.property 에서 오류난 필드에 대해서 얻어오면서 객체의 키값을 적어주고

[error.property] 이거 표현법  키값 동적으로 할당하는 방법이에요..!

 

{
  phoneNumber: [
    '010으로 시작하는 숫자만 들어와야 합니다.',
    'phoneNumber must be longer than or equal to 11 characters'
  ]
}

죠렇게 만들어준후에 필터 한번 거치고 ( constraints 가 옵셔널로 설정되어서 한번 걸러줬습니다... )

reduce를 통해서 객체를 점점 머지하는 형태로 작성했습니다!

 

필드가 여러개에서 오류가 날수있기때문에 ( ValidationError 타입은 한 필드 의 오류 내용이고 어레이로 받으니깐 )

map,filter,reduce로 작성을 했습니다 그후에 생성자로 HttpException 으로 에러 내용을 넘겨줬다.

 

글로벌 exception필터에서 CustomValidationError를 잡아서 응답 반환하기


// 다른 로직 많은데 매우매우 간략화한 소스입니당..
@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
  async catch(exception: Error, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const request = ctx.getRequest<Request>();

    let statusCode: number;
    let error: HttpExceptionErrorResponseDto;
    
    if (exception instanceof CustomValidationError) {
      statusCode = exception.getStatus();
      const getError = exception.getResponse();
      const objError = getError as ValidationErrorResponseDto;
      error = {
        ...objError
      };
    } 

    const errorResponse: ErrorCommonResponse<HttpExceptionErrorResponseDto> = {
      statusCode: statusCode,
      timestamp: new Date(),
      path: request.url,
      method: request.method,
      error: error
    };

    return response.status(statusCode).json(errorResponse);
  }
}

src/common/exceptions/http-exception.filter.ts

 

 간단하다 고티켓은 공통에러 부분을 정의내리고 사용중이므로 ErrorCommonResponse를 사용중이다..

실행 컨텍스트 중에서 ArgumentsHost 인터페이서 형식으로 얻어오고, 거기서 request는 500번대 에러받으면 슬랙으로 요청 바디가져오는 용도나 request.url , request.method 가져오고.

 

중요한건 중간 if부분에 instanceof CustomValidationError인지를 검사해서  전에 생성자로 넘어준 정보들을 스프레딩해서 뿌려준다는 정도?

그러면 다음과 같은 응답결과를 얻을 수있다.

 

 + 별첨 : 스웨거 용 에러 응답 디티오 & 스웨거... 이거 어떻게 작성할까..?


그래서 뭐 응답값 주는건 알겠는데... 저흰... 스웨거로 문서를 전달을 해줘야하잖아유?

공통응답은 오케이인데... error 객체안에

HttpExceptionErrorResponseDto, ValidationErrorResponseDto 요 두놈이있는데...

제네릭이있네,,,,?

 

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/HttpExceptionError.response.dtots

 

ErrorCommonResponse 와 제네릭을 사용해서 응답값을 표출해주는데

위 응답값이 기본 공통응답이다.

EnumToArray는 숫자형 이넘이 스웨거에서 말을 안들어서 만든거고

error 필드의 type : "generic" 으로 선언한거는 ... 사실 스웨거가

제네릭에대해서 잘 안지원해준다. 사실 물리적으로 불가능에 가까운거긴하다 왜냐면 제네릭은 런타임에 결정되는거니깐....

그래서 스웨거에게 어떤값이 응답으로 줘야할지는 직접 알려줘야하는데

/**
 * 제네릭을 활용한 swagger 응답을 주기위한 용도입니다.
 * @param props model : 원래 응답할 data의 타입 , description 응답 설명
 * @returns
 */
export const ApiPaginatedDto = <TModel extends Type<any>>(props: {
  model: TModel;
  description?: string;
}) => {
  return applyDecorators(
    ApiExtraModels(PageDto, props.model),
    ApiOkResponse({
      description: props.description,
      schema: {
        allOf: [
          { $ref: getSchemaPath(PageDto) },
          {
            properties: {
              data: {
                type: 'array',
                items: { $ref: getSchemaPath(props.model) }
              }
            }
          }
        ]
      }
    })
  );
};

 src/common/decorators/ApiPaginatedDto.decorator.ts

 

페이지네이션 소스이긴한데

저런식으로 ApiExtraModels를 선언하고 ,  model로 ref를 넘겨줘야한다.

뭐이건 나중에 포스팅으로 자세히 전달하겠다. 제네릭 부분에서 부터 약간 자율성이 떨어진달까... 내가 원하는대로커스텀을 하기 힘들다.

+같은 응답 코드면 여러개를 못다는데...

그래서 @ApiProperty 의 메타데이터 를 이용해서 제네릭까지 대응 할수 있는 스웨거용 커스텀 데코레이터를 만들었다.

차후 포스팅을 한뒤에 링크 달아드릴께유..

만들면 약간 이렇게 된다.

ㅋㅋ요래 만들수있따!

 

 

----------요기에 링크넣을꺼얌----------


요번엔 에러 코드 정의까지 내려봐서 좀 정형화를 하려고 노력도 해보고..

스웨거 쓰면 사실 예시나 그런걸 적기 참 뭔가 대충하게 되는 느낌이 있는데 그런걸 최대한 제해 볼려고 노력했었다.

스프링이 최고야 역시

 

 

GitHub - Gosrock/Ticket-Backend-22nd

Contribute to Gosrock/Ticket-Backend-22nd development by creating an account on GitHub.

github.com

 필요한 소스들은 src/common 영역쪽에 있으니 참고하시길 바란다!