Skip to content
NestJS ns pipes 4 min read

Validation with class-validator

NestJS leans on the class-validator library to express validation rules as decorators directly on your DTO properties. Instead of writing imperative if checks in every controller, you annotate each field once and let the ValidationPipe enforce those rules automatically. This keeps validation declarative, co-located with the data shape, and reusable across the whole request lifecycle.

Setup

class-validator works hand-in-hand with class-transformer, which converts plain JSON payloads into instances of your DTO classes so decorators can actually run against them.

npm install class-validator class-transformer

Then enable the global ValidationPipe so every incoming request body is validated:

// main.ts
import { NestFactory } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(
    new ValidationPipe({
      whitelist: true,
      forbidNonWhitelisted: true,
      transform: true,
    }),
  );
  await app.listen(3000);
}
bootstrap();

Common validation decorators

Each decorator attaches a constraint to a property. When the pipe runs, every failing constraint contributes a message to the resulting 400 Bad Request. Here is a representative DTO using the most frequently reached-for decorators.

// create-user.dto.ts
import {
  IsString,
  IsEmail,
  IsInt,
  Min,
  Max,
  IsOptional,
  IsEnum,
  Length,
  IsUrl,
  IsBoolean,
} from 'class-validator';

export enum UserRole {
  Admin = 'admin',
  Member = 'member',
}

export class CreateUserDto {
  @IsString()
  @Length(2, 40)
  name: string;

  @IsEmail()
  email: string;

  @IsInt()
  @Min(18)
  @Max(120)
  age: number;

  @IsEnum(UserRole)
  role: UserRole;

  @IsUrl()
  @IsOptional()
  website?: string;

  @IsBoolean()
  @IsOptional()
  newsletter?: boolean;
}

The table below summarizes the decorators you will reach for most often.

DecoratorPasses when the value is…
@IsString()a string
@IsInt() / @IsNumber()an integer / any number
@IsBoolean()a boolean
@IsEmail()a valid email address
@IsUrl()a valid URL
@IsEnum(E)a member of enum E
@Min(n) / @Max(n)a number >= n / <= n
@Length(min, max)a string within the length range
@IsOptional()absent (skips other validators when undefined)
@IsArray()an array
@Matches(regex)matching the supplied pattern

@IsOptional() does more than mark a field optional — it short-circuits all other decorators on that property when the value is null or undefined. Without it, an omitted optional field still triggers @IsString() and friends.

Nested object validation

Validators do not recurse into nested objects by default. To validate a child object or an array of child objects, combine @ValidateNested() with @Type() from class-transformer so the nested payload is instantiated as the correct class.

// create-order.dto.ts
import { Type } from 'class-transformer';
import {
  IsString,
  IsInt,
  Min,
  ValidateNested,
  ArrayMinSize,
} from 'class-validator';

export class AddressDto {
  @IsString()
  street: string;

  @IsString()
  city: string;
}

export class OrderItemDto {
  @IsString()
  sku: string;

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

export class CreateOrderDto {
  @ValidateNested()
  @Type(() => AddressDto)
  shippingAddress: AddressDto;

  @ValidateNested({ each: true })
  @ArrayMinSize(1)
  @Type(() => OrderItemDto)
  items: OrderItemDto[];
}

The { each: true } option tells the validator to apply nested validation to every element of the array. @Type(() => OrderItemDto) is essential — without it, class-transformer leaves the items as plain objects and the nested decorators never fire.

Conditional validation

Sometimes a field is only required depending on the value of another field. @ValidateIf() enables or disables all subsequent validators based on a predicate.

// payment.dto.ts
import { IsString, IsEnum, ValidateIf, IsNotEmpty } from 'class-validator';

export enum PaymentMethod {
  Card = 'card',
  Invoice = 'invoice',
}

export class PaymentDto {
  @IsEnum(PaymentMethod)
  method: PaymentMethod;

  // Only required when paying by card.
  @ValidateIf((dto) => dto.method === PaymentMethod.Card)
  @IsString()
  @IsNotEmpty()
  cardNumber?: string;

  // Only required when invoicing.
  @ValidateIf((dto) => dto.method === PaymentMethod.Invoice)
  @IsString()
  @IsNotEmpty()
  companyTaxId?: string;
}

Custom error messages

Every constraint decorator accepts an options object with a message field. You can pass a static string or a function that receives validation arguments for dynamic messages.

import { IsString, MinLength, Min } from 'class-validator';

export class SignupDto {
  @IsString({ message: 'Username must be text.' })
  @MinLength(3, {
    message: ({ value, constraints }) =>
      `Username "${value}" is too short; need at least ${constraints[0]} characters.`,
  })
  username: string;

  @Min(0, { message: 'Credits cannot be negative.' })
  credits: number;
}

When validation fails, the ValidationPipe aggregates the messages into the response body.

Output:

{
  "statusCode": 400,
  "message": [
    "Username \"jo\" is too short; need at least 3 characters.",
    "Credits cannot be negative."
  ],
  "error": "Bad Request"
}

Best Practices

  • Always enable whitelist: true so unknown properties are stripped, and forbidNonWhitelisted: true to reject payloads carrying extra fields.
  • Pair the pipe with transform: true and @Type() so JSON values arrive as the correct types (numbers, dates, nested classes).
  • Reach for @IsOptional() on every nullable field — it prevents spurious errors on omitted properties.
  • Never forget @Type(() => Child) alongside @ValidateNested(); without it nested rules silently do nothing.
  • Keep separate DTOs for create and update operations rather than overloading one class with @IsOptional() everywhere.
  • Centralize reusable validation logic in custom decorators rather than duplicating long constraint chains across DTOs.
Last updated June 14, 2026
Was this helpful?