[NestJS] Pipes (Validation, Transformation)
정의
NestJS Pipe 는 controller method 로 들어가는 인자를 변환 (transformation) 하거나 검증 (validation) 하는 클래스입니다. Guard/Interceptor 이후, handler 이전에 실행됩니다. 두 가지 관용:
- 변환:
ParseIntPipe(string -> number) - 검증:
ValidationPipe(DTO 검증)
내장 Pipe
import {
ParseIntPipe,
ParseFloatPipe,
ParseBoolPipe,
ParseArrayPipe,
ParseUUIDPipe,
ParseDatePipe, // v11 신규
ParseEnumPipe,
DefaultValuePipe,
ValidationPipe,
} from '@nestjs/common';
@Get(':id')
find(@Param('id', ParseIntPipe) id: number) { ... }
@Get()
list(
@Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number,
@Query('active', new DefaultValuePipe(true), ParseBoolPipe) active: boolean,
) { ... }
@Get(':uuid')
findByUuid(@Param('uuid', ParseUUIDPipe) uuid: string) { ... }
@Get()
byDate(@Query('date', ParseDatePipe) date: Date) { ... }
@Get()
byIds(
@Query('ids', new ParseArrayPipe({items: Number, separator: ','}))
ids: number[],
) { ... }
파싱 실패 시 자동 400 BadRequest.
ValidationPipe (DTO 검증)
class-validator + class-transformer 필요:
npm i class-validator class-transformer
전역 활성화
// main.ts
app.useGlobalPipes(new ValidationPipe({
whitelist: true, // DTO 에 없는 필드 제거
forbidNonWhitelisted: true, // 있으면 오류 (선택)
transform: true, // 자동 변환 (string -> number 등)
transformOptions: {
enableImplicitConversion: true,
},
}));
DTO 정의
import {
IsEmail,
IsInt,
IsString,
IsOptional,
MinLength,
MaxLength,
Min,
Max,
IsEnum,
IsDate,
ValidateNested,
IsArray,
} from 'class-validator';
import { Type } from 'class-transformer';
export enum Role {
Admin = 'admin',
User = 'user',
Guest = 'guest',
}
export class CreateUserDto {
@IsEmail()
email: string;
@IsString()
@MinLength(8)
@MaxLength(128)
password: string;
@IsString()
@MinLength(1)
@MaxLength(50)
name: string;
@IsInt()
@Min(0)
@Max(150)
age: number;
@IsEnum(Role)
role: Role;
@IsDate()
@Type(() => Date) // string -> Date 자동 변환
birthDate: Date;
@IsOptional()
@IsString()
bio?: string;
@ValidateNested()
@Type(() => AddressDto)
address: AddressDto;
@IsArray()
@IsString({each: true})
@ArrayMinSize(1)
tags: string[];
}
사용
@Post()
create(@Body() dto: CreateUserDto) {
// 이미 validated + transformed
// birthDate 는 Date 인스턴스
// ...
}
검증 실패 시 자동 400:
{
"statusCode": 400,
"message": [
"email must be an email",
"password must be longer than or equal to 8 characters"
],
"error": "Bad Request"
}
Query DTO 검증
export class ListUsersQuery {
@Type(() => Number)
@IsInt()
@Min(1)
@IsOptional()
page?: number = 1;
@Type(() => Number)
@IsInt()
@Min(1)
@Max(100)
@IsOptional()
limit?: number = 10;
@IsOptional()
@IsEnum(['asc', 'desc'])
order?: 'asc' | 'desc' = 'desc';
}
@Type(() => Number) 는 class-transformer 로 string -> number.
Custom Pipe
import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common';
@Injectable()
export class TrimPipe implements PipeTransform<string, string> {
transform(value: string, metadata: ArgumentMetadata): string {
if (typeof value !== 'string') return value;
return value.trim();
}
}
@Injectable()
export class ParsePositiveIntPipe implements PipeTransform<string, number> {
transform(value: string): number {
const n = parseInt(value, 10);
if (isNaN(n) || n <= 0) {
throw new BadRequestException('Must be a positive integer');
}
return n;
}
}
Pipe 부착 방법
Parameter
@Get(':id')
find(@Param('id', ParseIntPipe) id: number) { ... }
// 옵션 있는 pipe
@Get(':id')
find(
@Param('id', new ParseIntPipe({errorHttpStatusCode: HttpStatus.NOT_ACCEPTABLE}))
id: number,
) { ... }
// 여러 pipe (순서대로)
@Get()
list(@Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number) { ... }
Method
@Post()
@UsePipes(new ValidationPipe({transform: true}))
create(@Body() dto: CreateUserDto) { ... }
Controller
@Controller('users')
@UsePipes(new ValidationPipe())
export class UsersController { ... }
Global
// main.ts
app.useGlobalPipes(new ValidationPipe({...}));
// 또는 module
@Module({
providers: [
{ provide: APP_PIPE, useClass: ValidationPipe },
],
})
export class AppModule {}
Transform 옵션
@Type() 없이 자동 변환:
new ValidationPipe({
transform: true,
transformOptions: {
enableImplicitConversion: true, // TS 타입 기반 자동 변환
},
})
이제 page: number 필드가 string 으로 들어와도 자동으로 Number. 위험도 있음 ("abc" -> NaN).
Whitelist / ForbidNonWhitelisted
new ValidationPipe({
whitelist: true, // DTO 에 없는 필드 자동 제거
forbidNonWhitelisted: true, // 있으면 오류
})
- whitelist: 안전 (extra 필드 무시)
- forbidNonWhitelisted: 엄격 (스키마 강제, 오탈자 방지)
Nested Validation
export class AddressDto {
@IsString() city: string;
@IsString() zipCode: string;
}
export class CreateUserDto {
@IsEmail() email: string;
@ValidateNested()
@Type(() => AddressDto)
address: AddressDto;
@IsArray()
@ValidateNested({each: true})
@Type(() => AddressDto)
addresses: AddressDto[];
}
Groups / Conditional
export class UpdateUserDto {
@IsOptional()
@IsEmail({}, {groups: ['update']})
email?: string;
}
new ValidationPipe({groups: ['update']})
Async Custom Validator
import { registerDecorator, ValidationOptions, ValidatorConstraint, ValidatorConstraintInterface } from 'class-validator';
@ValidatorConstraint({name: 'IsEmailUnique', async: true})
export class IsEmailUniqueConstraint implements ValidatorConstraintInterface {
constructor(private usersService: UsersService) {}
async validate(email: string) {
const exists = await this.usersService.emailExists(email);
return !exists;
}
}
export function IsEmailUnique(options?: ValidationOptions) {
return function(object: object, propertyName: string) {
registerDecorator({
target: object.constructor,
propertyName,
options,
validator: IsEmailUniqueConstraint,
});
};
}
주의: Async custom validator 는 useContainer 설정 필요.
Zod 대체
class-validator 대신 zod 사용 가능. NestJS 는 nestjs-zod 등 통합 라이브러리.
import { z } from 'zod';
import { createZodDto } from 'nestjs-zod';
const CreateUserSchema = z.object({
email: z.string().email(),
password: z.string().min(8),
age: z.number().int().min(0).max(150),
});
export class CreateUserDto extends createZodDto(CreateUserSchema) {}
TypeScript 타입 자동 추론. 런타임 검증.
v11 신규: ParseDatePipe
@Get()
byDate(@Query('date', new ParseDatePipe()) date: Date) { ... }
// 옵션
@Get()
byDate(
@Query('date', new ParseDatePipe({
optional: true,
default: () => new Date(),
}))
date: Date,
) { ... }
ISO 8601 문자열 -> Date. 실패 시 400.
함정
WARNING
ValidationPipe global 등록 안 하면 DTO 검증 안 됨. main.ts 에 반드시.
CAUTION
transform: false 상태에서 @Type() 무력. transform 필수.
WARNING
class-validator metadata 필요. reflect-metadata import + tsconfig emitDecoratorMetadata: true.
IMPORTANT
Nested DTO 는 @ValidateNested() + @Type() 필수. 안 하면 nested 는 검증 skip.
CAUTION
enableImplicitConversion 은 위험할 수도. age: number 필드에 "abc" 오면 NaN. 명시적 @Type(() => Number) 가 안전.
관련 위키
- NestJS - 상위 개요
- Controllers -
@UsePipes() - Guards - 이전 단계
- Interceptors
- Exception Filters - 검증 실패 처리
- Modules
- Providers
- FastAPI Pydantic - 대비
- TypeScript Decorators
이 글의 용어 (9개)
- [FastAPI] Pydantic Integrationfastapi
- 정의 FastAPI + Pydantic 은 요청/응답의 타입 검증, 직렬화, JSON Schema 생성 을 위한 통합입니다. Pydantic v2 는 Rust 코어 ( ) 로 v…
- [Framework] NestJSframeworks
- 정의 NestJS 는 Kamil Myśliwiec 이 2017년 발표한 Node.js 서버측 프레임워크 입니다. TypeScript first, Angular 에서 영감받은 모듈…
- [NestJS] Controllersframeworks
- 정의 NestJS Controller 는 데코레이터가 붙은 클래스로, HTTP 요청을 라우팅하고 응답을 반환합니다. Method 별로 , , , , 데코레이터를 붙여 URL + …
- [NestJS] Exception Filtersframeworks
- 정의 NestJS Exception Filter 는 애플리케이션에서 발생한 예외를 catch 하여 클라이언트에게 응답 형식을 통일합니다. 데코레이터로 어떤 예외를 처리할지 지정합…
- [NestJS] Guardsframeworks
- 정의 NestJS Guard 는 특정 요청이 controller method 에 도달하기 전에 실행되어 접근 허용 여부 를 결정합니다. Middleware 이후, Intercep…
- [NestJS] Interceptorsframeworks
- 정의 NestJS Interceptor 는 요청 처리 전/후에 로직을 삽입하는 클래스입니다. AOP (Aspect-Oriented Programming) 패턴의 구현체로, 로깅,…
- [NestJS] Modulesframeworks
- 정의 NestJS Module 은 데코레이터가 붙은 클래스로, 관련된 controller, provider, import/export 를 하나로 묶는 조직 단위입니다. Angul…
- [NestJS] Providers & Dependency Injectionframeworks
- 정의 NestJS Provider 는 데코레이터가 붙은 클래스 (또는 값/factory) 로, DI container 에 등록되어 다른 클래스에 주입됩니다. Service, Re…
- [TypeScript] Decoratorstypescript
- 정의 Decorator 는 class, method, accessor, property, parameter 에 부착되는 함수로 그 대상을 검사/수정할 수 있습니다. TypeScr…
💬 댓글