On this page

Pipes and validation with class-validator

14 min read TextCh. 2 — Services and Injection

What are pipes?

A pipe in NestJS is a class that implements PipeTransform and is used to transform or validate data flowing through the request pipeline. Pipes execute after middleware and guards but before the route handler.

The two main use cases for pipes are:

  1. Transformation — convert input data into the desired format (e.g., parse a string ID to a number)
  2. Validation — evaluate the input and throw an exception if it does not meet the requirements

Built-in pipes

NestJS ships with several built-in pipes:

Pipe Purpose
ValidationPipe Validates request bodies using class-validator decorators
ParseIntPipe Parses and validates a value as an integer
ParseFloatPipe Parses and validates a value as a float
ParseBoolPipe Parses and validates a value as a boolean
ParseArrayPipe Parses and validates a value as an array
ParseUUIDPipe Validates a value as a UUID
ParseEnumPipe Validates a value against an enum
DefaultValuePipe Provides a default value if the input is undefined

Using built-in pipes on route parameters:

@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number) {
  return this.booksService.findOne(id);
}

@Get()
findAll(
  @Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number,
  @Query('limit', new DefaultValuePipe(10), ParseIntPipe) limit: number,
) {
  return this.booksService.findAll(page, limit);
}

Validation with class-validator

The most common use of pipes in NestJS is validating request bodies with ValidationPipe and class-validator. Install the required packages:

npm install class-validator class-transformer

Then define DTOs (Data Transfer Objects) with validation decorators:

import { IsString, IsNotEmpty, IsNumber, IsPositive } from 'class-validator';

export class CreateBookDto {
  @IsString()
  @IsNotEmpty()
  title: string;

  @IsNumber()
  @IsPositive()
  price: number;
}

When a request arrives with an invalid body, ValidationPipe throws a BadRequestException with detailed error messages about which fields failed and why.

Registering ValidationPipe globally

You should register ValidationPipe globally in main.ts so it applies to all routes:

app.useGlobalPipes(
  new ValidationPipe({
    whitelist: true,
    forbidNonWhitelisted: true,
    transform: true,
  }),
);

The three options above form the recommended production configuration:

  • whitelist — Strips any properties not defined in the DTO. If a client sends { title: 'Clean Code', _secret: 'hack' }, the _secret field is silently removed.
  • forbidNonWhitelisted — Instead of silently stripping, throws a 400 Bad Request when unknown properties are present.
  • transform — Automatically converts plain JSON objects into DTO class instances. Without this, body would be a plain object and the validation decorators would still work, but your service would receive a plain object, not a CreateBookDto instance.

Useful class-validator decorators

String validators

@IsString()           // Must be a string
@IsNotEmpty()         // Cannot be empty string
@IsEmail()            // Valid email format
@IsUrl()              // Valid URL
@IsUUID()             // Valid UUID v4
@MaxLength(200)       // Max 200 characters
@MinLength(3)         // Min 3 characters
@Matches(/^[A-Z]/)    // Matches regex

Number validators

@IsNumber()           // Must be a number
@IsInt()              // Must be an integer
@IsPositive()         // Greater than 0
@IsNegative()         // Less than 0
@Min(0)               // Greater than or equal to 0
@Max(100)             // Less than or equal to 100

Boolean and date validators

@IsBoolean()          // Must be a boolean
@IsDate()             // Must be a Date instance
@IsDateString()       // Must be an ISO 8601 date string

Array and object validators

@IsArray()            // Must be an array
@ArrayMinSize(1)      // At least 1 element
@ArrayMaxSize(10)     // At most 10 elements
@ArrayUnique()        // All elements must be unique

@IsObject()           // Must be an object
@ValidateNested({ each: true }) // Validate nested objects
@Type(() => CreateBookDto)      // Required for ValidateNested

Optional and conditional

@IsOptional()         // Skips validation if value is null/undefined
@ValidateIf((o) => o.type === 'ebook')  // Conditional validation

Nested validation

When a DTO contains nested objects, use @ValidateNested together with @Type:

import { Type } from 'class-transformer';

export class CreateOrderDto {
  @ValidateNested({ each: true })
  @Type(() => OrderItemDto)
  @IsArray()
  items: OrderItemDto[];
}

export class OrderItemDto {
  @IsNumber()
  @IsPositive()
  bookId: number;

  @IsInt()
  @Min(1)
  quantity: number;
}

Without @Type(() => OrderItemDto), class-transformer does not know the type of the nested object and ValidateNested has no decorators to check.

Creating custom pipes

When built-in pipes and class-validator are not enough, you can create your own:

@Injectable()
export class TrimPipe implements PipeTransform<string, string> {
  transform(value: string): string {
    if (typeof value !== 'string') return value;
    return value.trim();
  }
}

Apply it at the parameter, method, or controller level:

// Parameter level
@Post()
create(@Body('title', TrimPipe) title: string) {}

// Method level
@Post()
@UsePipes(TrimPipe)
create(@Body() dto: CreateBookDto) {}

// Controller level
@Controller('books')
@UsePipes(TrimPipe)
export class BooksController {}

ParseEnumPipe for query parameters

When you have a route that accepts a limited set of values, ParseEnumPipe provides clear validation:

enum BookSort {
  NEWEST = 'newest',
  PRICE_ASC = 'price_asc',
  PRICE_DESC = 'price_desc',
}

@Get()
findAll(@Query('sort', new ParseEnumPipe(BookSort)) sort: BookSort) {
  return this.booksService.findAll(sort);
}

Requesting /books?sort=invalid returns 400 Bad Request: sort must be one of the following values: newest, price_asc, price_desc.

Custom validation decorators

You can create reusable custom validators with registerDecorator:

import {
  registerDecorator,
  ValidationOptions,
  ValidatorConstraint,
  ValidatorConstraintInterface,
} from 'class-validator';

@ValidatorConstraint({ async: false })
class IsIsbnConstraint implements ValidatorConstraintInterface {
  validate(value: string): boolean {
    // Custom ISBN-13 validation logic
    return /^\d{13}$/.test(value.replace(/-/g, ''));
  }

  defaultMessage(): string {
    return 'isbn must be a valid ISBN-13';
  }
}

function IsValidIsbn(options?: ValidationOptions) {
  return (object: object, propertyName: string) => {
    registerDecorator({
      target: object.constructor,
      propertyName,
      options,
      constraints: [],
      validator: IsIsbnConstraint,
    });
  };
}

// Usage in DTO
export class CreateBookDto {
  @IsValidIsbn()
  isbn: string;
}
whitelist: true is essential
Always enable whitelist: true in ValidationPipe. Without it, clients can send extra properties that bypass validation and potentially cause security issues or unexpected behavior in your services. Combined with forbidNonWhitelisted: true, your DTOs act as a strict contract.
transform: true has a cost
Enabling transform: true in ValidationPipe makes NestJS instantiate DTO classes for every request, which adds a small overhead. For high-throughput endpoints, consider disabling it and using plain objects validated against the DTO schema instead.
import {
  IsString,
  IsNotEmpty,
  IsNumber,
  IsPositive,
  IsISBN,
  IsOptional,
  IsDateString,
  MaxLength,
  Min,
  Max,
} from 'class-validator';

export class CreateBookDto {
  @IsString()
  @IsNotEmpty()
  @MaxLength(200)
  title: string;

  @IsISBN()
  isbn: string;

  @IsNumber({ maxDecimalPlaces: 2 })
  @IsPositive()
  @Max(9999.99)
  price: number;

  @IsNumber()
  @IsPositive()
  authorId: number;

  @IsNumber()
  @IsOptional()
  categoryId?: number;

  @IsDateString()
  @IsOptional()
  publishedAt?: string;
}