[고스락 티켓 2.0] nest js ValidationError custom
nest js 에서는 검증도중에 오류가 발생하면 400번 ValidationError를 발생시키는데
{
"statusCode": 400,
"error": "Bad Request",
"message": ["email must be an email"]
}
이런 형식으로 나온다.
사실뭐 그냥 써도 상관없지만 ( 보통 클라단에서 다막기 때문 )
클라이언트가 보낸 필드중에 어느부분이 오류인지 알려면
검증 오류가 난 필드가 무엇이고, 그필드에 해당하는 오류내용이 무엇인지 아래와같은 형식으로 기술을 할려고 한다.
( code 부분은 다른 HttpExcepiton 을 상속받은 에러들에서 에러코드를 나타내기 위해 만든것인데 , 공통으로 맞춰줄려고 ValidationError 라고 적었다 )
이글을 통해서 얻어갈 수 있는 것들
- useGlobalPipes 에서 exceptionFactory를 통해서 커스텀에러형식을 반환하는 방법
- CustomValidationError 정의내리기
- 글로벌 exception필터에서 CustomValidationError를 잡아서 응답 반환하기
이글의 예시들은 고스락 티켓 예매 프로젝트 22nd 에서 사용중인 코드들이다.
프로젝트에 관련한 내용은 위글과 , 깃허브의 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의 실행 순서에 관한 부분인데 궁금하시다면
위글 중반부의 class-validation 과 class-transformer의 실행 시점에 관한 이야기를 보고오시면 될것같다.
커스텀으로 정의 내리기위해선 exceptionFactory를 통하면 되는데
ValidationError[] 형으로 인자를받고 일단 CustomValidaitonError의 생성자로 넘겨주었다.
우리는 저 부분에서 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 의 메타데이터 를 이용해서 제네릭까지 대응 할수 있는 스웨거용 커스텀 데코레이터를 만들었다.
차후 포스팅을 한뒤에 링크 달아드릴께유..
ㅋㅋ요래 만들수있따!
----------요기에 링크넣을꺼얌----------
요번엔 에러 코드 정의까지 내려봐서 좀 정형화를 하려고 노력도 해보고..
스웨거 쓰면 사실 예시나 그런걸 적기 참 뭔가 대충하게 되는 느낌이 있는데 그런걸 최대한 제해 볼려고 노력했었다.
스프링이 최고야 역시
필요한 소스들은 src/common 영역쪽에 있으니 참고하시길 바란다!