Skip to content
TypeScript ts nestjs 3 min read

Validation with class-validator

NestJS validates incoming data by combining three pieces: class-validator decorators that declare the rules, class-transformer that turns the raw JSON payload into a real DTO instance, and the ValidationPipe that ties them together and throws a 400 Bad Request on failure. Because validation reads decorator metadata at runtime, your DTOs must be classes (interfaces are erased). This page shows how to install and wire the pipe, the most useful validation decorators, and the critical whitelist and transform options that turn a permissive pipe into a strict, secure one.

Installing and enabling validation

The two libraries are peer packages; install both, then register a single global ValidationPipe so every endpoint is covered.

npm install class-validator class-transformer
import { ValidationPipe } from "@nestjs/common";
import { NestFactory } from "@nestjs/core";
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();

A global pipe means you never repeat validation wiring per controller. The three options shown are the ones that matter most; the next sections explain each.

Declaring rules on a DTO

Each property carries the decorators describing its constraints. The pipe runs every rule and aggregates the failures into one response.

import { IsEmail, IsInt, IsOptional, Length, Min } from "class-validator";

export class CreateUserDto {
  @IsEmail()
  email!: string;

  @Length(8, 64)
  password!: string;

  @IsInt()
  @Min(18)
  age!: number;

  @IsOptional()
  @Length(0, 280)
  bio?: string;
}

@IsOptional() tells the validator to skip a field when it is undefined, so optional properties are not flagged as missing. Stack as many rule decorators as you need; they all run.

Common validation decorators

class-validator ships dozens of rules. These are the ones you reach for daily.

DecoratorChecks
@IsString(), @IsInt(), @IsBoolean()primitive type
@IsEmail(), @IsUrl(), @IsUUID()string format
@Min(n), @Max(n), @Length(min, max)numeric / length range
@IsOptional()skip rules when value is absent
@IsEnum(E)value belongs to an enum
@ValidateNested() + @Type()recurse into nested DTOs

@IsEnum is worth highlighting: it validates against a runtime enum object, which is one of the few cases where a TypeScript enum (rather than a union of literals) earns its runtime footprint.

whitelist and forbidNonWhitelisted

whitelist: true strips any property that has no validation decorator, so attackers cannot smuggle extra fields (like isAdmin) into your DTO. forbidNonWhitelisted: true goes further and rejects the request outright when unknown properties appear.

// Request body: { "email": "[email protected]", "password": "secret123", "isAdmin": true }

// whitelist: true            → isAdmin silently removed
// forbidNonWhitelisted: true → 400 "property isAdmin should not exist"

Without whitelist, every undeclared field in the payload flows straight through to your service and database. Treat whitelist: true as a security baseline, not an optional nicety.

transform: real instances and coerced types

By default the DTO your handler receives is a plain object, not an instance of the class. Setting transform: true makes the pipe use class-transformer to produce a true instance and to coerce primitives — turning a route param string into a number when the type says number.

import { Type } from "class-transformer";
import { IsInt, Min } from "class-validator";

export class PaginationDto {
  @Type(() => Number) // query strings are text; convert before validating
  @IsInt()
  @Min(1)
  page = 1;
}

With transform: true, ?page=2 arrives as the number 2, and page instanceof PaginationDto is true. The explicit @Type(() => Number) is needed for query and param values because they come in as strings and @IsInt() alone would reject them.

Best Practices

  • Register one global ValidationPipe in main.ts instead of decorating every controller.
  • Always enable whitelist: true to strip undeclared fields; add forbidNonWhitelisted to reject them.
  • Enable transform: true so handlers receive real DTO instances with coerced primitive types.
  • Use @Type(() => Number) on numeric query/param DTO fields before @IsInt()/@Min().
  • Reach for @IsOptional() on optional fields so absent values are not flagged.
  • Validate nested objects with @ValidateNested() + @Type(); arrays need { each: true }.
  • Keep DTOs as classes — class-validator reads runtime metadata that interfaces cannot provide.
Last updated June 29, 2026
Was this helpful?