기준: nestjs 10.3.3, class-validator 0.14.1, class-transformer 0.5.1
NestJS 공식 문서에서 아래와 같은 코드를 사용하고 있다.
@Get(':id')
findOne(@Param() params: FindOneParams) {
return 'This action returns a user';
}
object의 프로퍼티 id
로 endpoint path의 :id
값을 받고 있다.
(object를 통하지 않고 직접 받는 방법에 관해서도 아래에서 다룸)
import { IsNumberString } from 'class-validator';
export class FindOneParams {
@IsNumberString()
id: number;
}
number
타입인 id
로 들어오는 데이터가 number string인지 validate하고 있다.
❓ 그런데 이 코드는 의도대로 동작하지 않는다.
위처럼 id
타입을 number
로 설정하더라도 실제로는 number string
값이 저장되지 자동으로 number
로 변환되지 않는다.
게다가, 아마 양의 정수만 받는 상황일 것 같은데, -123.45
같은 값도 그대로 저장된다.
그럼 이 경우 어떻게 해야 id
의 타입에 맞게 자동 변환될까❓
❗️ 먼저 아래와 같은 방법으로 기능을 활성화해야 한다. 관련 깃헙 이슈
// main.ts
async function bootstrap() {
// ...
app.useGlobalPipes(
new ValidationPipe({
transform: true,
transformOptions: { enableImplicitConversion: true },
}),
);
// ...
}
이렇게 하고 나면 이제 number string
값이 number
로 자동 변환된다.
그런데 변환된 후에 validation이 적용되기 때문에 지금 상태로는 @IsNumberString()
에 막혀 에러 응답이 반환된다.
기존의 데코레이터를 지우고 양의 정수만 허용하도록 해 보자.
import { IsInt, IsPositive } from 'class-validator';
export class FindOneParams {
@IsInt()
@IsPositive()
id: number;
}
다 되었다. 이제 url에 id 값으로 양의 정수 형식의 값을 넣었을 때만 통과된다.
참고로,
양의 정수 형식이 아닌 값이 들어오더라도 일단은 number
로 변환되고(number string이면 값 그대로, 아니면 값이 NaN
으로), 그 후 validation이 적용된다.
위처럼 object를 통하지 않고 아래처럼 직접 받을 수도 있다.
@Get(':id')
findOne(@Param('id') id: number) {
return 'This action returns a user';
}
참고로, 이때는 ValidationPipe
에 전달하는 configuration object에 transform: true
만 있어도 자동으로 number
로 변환된다.
❓ 그런데 이러면 자동 validation이 안 된다.
@IsInt()
같은 것들을 파라미터 id
에 붙여줄 수 없다.
그럼 이 경우 자동 validation을 적용하려면 어떻게 해야 할까❓
❗️ pipe도 함께 사용해야 한다.
built-in pipe들 중 ParseIntPipe
를 두 가지 방법으로 사용해 보겠다.
@Get(':id')
@UsePipes(new ParseIntPipe()) // 추가
findOne(@Param('id') id: number) {
return 'This action returns a user';
}
@Get(':id')
findOne(@Param('id', new ParseIntPipe()) id: number) { // 전달할 option이 없을 때는 그냥 @Param('id', ParseIntPipe)처럼 좀 더 간단히 표현 가능
return 'This action returns a user';
}
❓ 그런데 이러면 음의 정수까지 허용된다.
양의 정수만 허용하려면 어떻게 해야 할까❓
❗️ 아래처럼 custom pipe를 만들어 사용해야 한다. ( ParseIntPipe
를 참고하여 작성해 보았음 )
// parse-positive-int.pipe.ts
import {
ArgumentMetadata,
HttpStatus,
Injectable,
PipeTransform,
} from '@nestjs/common';
import { HttpErrorByCode } from '@nestjs/common/utils/http-error-by-code.util';
@Injectable()
export class ParsePositiveIntPipe implements PipeTransform {
/**
* Method that accesses and performs transformation on argument for
* in-flight requests.
*
* @param value currently processed route argument
* @param metadata contains metadata about the currently processed route argument
*/
transform(value: any, metadata: ArgumentMetadata) {
if (!this.isNumeric(value)) {
throw new HttpErrorByCode[HttpStatus.BAD_REQUEST](
'Validation failed: numeric(positive integer) string is expected)',
);
}
return parseInt(value, 10);
}
/**
* @param value currently processed route argument
* @returns `true` if `value` is a valid positive integer number
*/
protected isNumeric(value: string): boolean {
return (
['string', 'number'].includes(typeof value) &&
/^(?!0$)\d+$/.test(value) && // 여기서 양의 정수만 허용
isFinite(value as any)
);
}
}
참고로, transform()
에서 반환하는 부분을 보면 알겠지만,
이렇게 pipe를 사용하면 ValidationPipe
에 전달하는 configuration object에 transform: true
가 없어도 자동으로 변환된다.