개발일지

[NestJS] ValidationPipe 커스텀하기 본문

NestJS, Node.js/#01 Project - 투표 커뮤니티

[NestJS] ValidationPipe 커스텀하기

lyjin 2022. 11. 30.

이전에 구현했던 프로젝트 기준으로 설명하겠습니다.

🔗 [NestJS] 회원가입 구현 #1 - Pipe 적용
🔗 [NestJS] 예외 처리와 exception filter

 


Dto 속성의 유효성 검사를 위해 ValidationPipeclass-validator를 사용했었습니다. 그런데 class-validator를 사용하면 에러 메시지가 배열 형태로 반환됩니다. 이를 exception filter에 적용한 것과 동일한 형태로 반환하고싶었기에 기존의 ValidationPipe를 커스텀했습니다.

 



ValidationPipe 내부 살펴보기

ValidationPipe 내부를 살펴보면 createExceptionFactory() 메소드가 존재하고, 함수 내의 flattenValidationErrors() 메소드 내에서 모든 validation 에러 메시지를 배열 형태로 변환하는 것을 알 수 있습니다. 더 자세한 내용은 여기를 참고해주세요.

 

 public createExceptionFactory() {
    return (validationErrors: ValidationError[] = []) => {
      if (this.isDetailedOutputDisabled) {
        return new HttpErrorByCode[this.errorHttpStatusCode]();
      }
      const errors = this.flattenValidationErrors(validationErrors);
      return new HttpErrorByCode[this.errorHttpStatusCode](errors);
    };
  }

  ...

  protected flattenValidationErrors(
    validationErrors: ValidationError[],
  ): string[] {
    return iterate(validationErrors)
      .map(error => this.mapChildrenToValidationErrors(error))
      .flatten()
      .filter(item => !!item.constraints)
      .map(item => Object.values(item.constraints))
      .flatten()
      .toArray();
  }

 

모든 에러 메시지를 배열에 담기보다는 제일 첫번째에 해당하는 에러 메세지만을 에러 코드와 함께 반환하고 싶었습니다. 따라서 flattenValidationErrors()를 사용하지 않고 원하는 형태로 변환해주는 메소드를 정의해주었습니다.

 


ValidationPipe 커스텀 하기

 

decorator에 context 전달하기

class-validator에서는 유효성 검사에 실패했을 경우의 속성들을 데코레이터에 지정할 수 있도록 합니다.
🔗 Passing context to decorators

 

 

Dto에 다음과 같이 에러 코드를 정의합니다.

 

export class SignUpUserDto {
  @IsEmail(
    {},
    {
      context: {
        code: 'MUST_EMAIL_TYPE',
      },
    },
  )
  @IsNotEmpty({
    context: {
      code: 'EMPTY_EMAIL',
    },
  })
  email: string;

  @IsString({
    context: {
      code: 'MUST_STRING_TYPE',
    },
  })
  @IsNotEmpty({
    context: {
      code: 'EMPTY_NICKNAME',
    },
  })
  nickname: string;
  ...
}
 

validationPipe 커스텀하기

기존의 ValidationPipe을 상속받는 CustomValidationPipe를 만들고 createExceptionFactory() 메소드를 오버라이딩 해줬습니다. getExceptionObj() 로 { code, message }를 갖는 에러 객체를 반환한 뒤 exception filter를 거치기위해 HttpException 에러를 던집니다.

 

// validation.pipe.ts

import {
  HttpException,
  ValidationError,
  ValidationPipe as NestValidationPipe,
} from '@nestjs/common';
import { HttpErrorByCode } from '@nestjs/common/utils/http-error-by-code.util';
import { ExceptionObj, OthersException } from '@vote/middleware';
import { CustomException } from '../exception.filter/http-exception.filter';

export class CustomValidationPipe extends NestValidationPipe {
  public createExceptionFactory() {
    return (validationErrors: ValidationError[] = []) => {
      if (this.isDetailedOutputDisabled) {
        return new HttpErrorByCode[this.errorHttpStatusCode]();
      }

      const errors = this.getExceptionObj(validationErrors);  // { message, code } 반환
      return new HttpException(errors, 400);
    };
  }
}

 

getExceptionObj() 메소드 입니다. 제일 처음 발생된 에러 객체(validationErrors[0])에서 기존의 에러 메시지(message)와 위에서 지정한 에러 코드(code)를 가져온 뒤 객체 형태로 반환합니다. 에러 코드가 지정되어있지 않을 경우 에러 처리됩니다.

 

  protected getExceptionObj(validationErrors: ValidationError[]): ExceptionObj {
    const error = validationErrors[0];

    // get code
    const errorCode = error.contexts;
    const key = errorCode ? Object.keys(errorCode)[0] : undefined;
    const code = errorCode?.[key]['code'];

    if (code === undefined) {
      throw new CustomException(OthersException.NOT_VALIDATION_ERROR_CODE);
    }

    // get message
    const message = error.constraints?.[key];

    return {
      code,
      message,
    };
  }

 

구현한 CustomValidationPipe를 기존의 파이프 대신 사용할 수 있도록 설정해줍니다.

 

// main.ts
async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(new CustomValidationPipe());
  ...

  await app.listen(configService.get('PORT'));
}
bootstrap();
 

exception filter 수정하기

controller 또는 service에서 에러가 발생했을 때와는 다르게 위에서 정의된 validation 에러의 경우 error stack 없이 바로 response 객체를 반환합니다. 따라서 두 경우의에 맞게 데이터를 받아올 수 있도록 했습니다.

 

@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
  catch(exception: HttpException, host: ArgumentsHost) {
    const httpRes = host.switchToHttp().getResponse<Response>();
    const status = exception.getStatus();
    const response = exception.getResponse();
    const errorRes = response?.['stack'] ? response?.['response'] : response;

    return httpRes.status(status).json({
      error: {
        code: errorRes?.['code'] || status,
        message: errorRes?.['message'],
      },
    });
  }
}

 

 


결과

커스텀 전

 

커스텀 후