본문으로 건너뛰기
김신건의 로그

[NestJS] Pipes (Validation, Transformation)

· 수정 · 📖 약 1분 · 389자/단어 #nestjs #pipe #validation #transformation
NestJS Pipe, NestJS Pipes, ValidationPipe, ParseIntPipe, ParseUUIDPipe, ParseDatePipe, class-validator, class-transformer, NestJS 검증

정의

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) 가 안전.

관련 위키

이 글의 용어 (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…

💬 댓글

사이트 검색 / 명령어

검색

스크롤 = 확대/축소 · 드래그 = 이동 · 0 = 원래 크기 · ESC = 닫기