Skip to content
NestJS ns pipes 4 min read

ValidationPipe

The built-in ValidationPipe is the workhorse of input validation in NestJS. Instead of hand-writing checks inside every controller, you describe the shape of valid data once on a DTO using class-validator decorators, and the pipe enforces those rules on every request automatically. When validation fails it throws a BadRequestException with a structured list of errors, so your handler only ever runs against data you trust. This keeps controllers thin and your validation rules colocated with the types they describe.

How ValidationPipe works

ValidationPipe reads the metatype of an argument — the TypeScript class declared in your handler signature — and runs class-validator against an instance of it. To do this it first uses class-transformer to turn the plain incoming object into a real class instance, then evaluates every validation decorator attached to that class. If any constraint fails, the request is rejected before the handler executes.

Two libraries do the heavy lifting and must be installed:

npm install class-validator class-transformer

A DTO is just a class annotated with validation decorators:

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

export class CreateUserDto {
  @IsString()
  @MinLength(2)
  name: string;

  @IsEmail()
  email: string;

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

Reference the DTO as the type of a @Body() parameter, and the pipe takes over:

import { Body, Controller, Post } from '@nestjs/common';
import { CreateUserDto } from './dto/create-user.dto';

@Controller('users')
export class UsersController {
  @Post()
  create(@Body() dto: CreateUserDto) {
    return { created: dto };
  }
}

Registering the pipe globally

You can bind ValidationPipe per-parameter or per-handler, but the idiomatic approach is to register it once globally so every DTO across the app is validated consistently.

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();

Now a malformed request is rejected automatically:

Output:

$ curl -X POST http://localhost:3000/users \
    -H 'Content-Type: application/json' \
    -d '{"name":"A","email":"not-an-email","age":15}'

{
  "statusCode": 400,
  "message": [
    "name must be longer than or equal to 2 characters",
    "email must be an email",
    "age must not be less than 18"
  ],
  "error": "Bad Request"
}

Register ValidationPipe globally in main.ts rather than decorating each handler. A single source of truth prevents the common bug where one forgotten endpoint silently accepts unvalidated input.

Key options

ValidationPipe accepts a ValidationPipeOptions object. The most impactful flags are below.

OptionDefaultEffect
whitelistfalseStrips any properties that have no validation decorator from the resulting object.
forbidNonWhitelistedfalseThrows an error instead of stripping when unknown properties are present (requires whitelist).
transformfalseConverts the payload into an instance of the DTO class and coerces primitive types.
forbidUnknownValuestrueRejects values that resolve to an unknown/empty metatype, blocking validation-bypass payloads.
disableErrorMessagesfalseOmits detailed messages from responses — useful in production to avoid leaking schema details.
transformOptionsPassed to class-transformer, e.g. enableImplicitConversion for automatic primitive coercion.
groupsRuns only the validation groups you specify, enabling context-specific rules.

whitelist and forbidNonWhitelisted

whitelist: true discards any field the DTO does not declare, protecting you against mass-assignment. Pairing it with forbidNonWhitelisted: true turns silent stripping into an explicit 400 so clients learn they sent unexpected data.

new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true });

Output:

$ curl -X POST http://localhost:3000/users \
    -H 'Content-Type: application/json' \
    -d '{"name":"Ada","email":"[email protected]","age":40,"isAdmin":true}'

{
  "statusCode": 400,
  "message": ["property isAdmin should not exist"],
  "error": "Bad Request"
}

transform and type coercion

By default a @Body() payload reaches your handler as a plain object, and route/query params arrive as strings. With transform: true the pipe returns a true instance of the DTO class — so instanceof checks and class methods work — and coerces declared primitive types.

new ValidationPipe({
  transform: true,
  transformOptions: { enableImplicitConversion: true },
});

With enableImplicitConversion, a query string like ?age=40 is converted to the number 40 based on the DTO’s TypeScript type, removing the need for an explicit @Type(() => Number) on simple cases.

Per-route and nested validation

Need different rules for a single endpoint? Pass a fresh instance to the parameter or handler:

import { Body, Controller, Post, UsePipes, ValidationPipe } from '@nestjs/common';

@Controller('imports')
export class ImportsController {
  @Post()
  @UsePipes(new ValidationPipe({ whitelist: false }))
  bulk(@Body() payload: CreateUserDto[]) {
    return { received: payload.length };
  }
}

For nested objects, decorate the property with @ValidateNested() and @Type() so class-transformer instantiates the child class before validation runs:

import { Type } from 'class-transformer';
import { ValidateNested } from 'class-validator';

export class CreateOrderDto {
  @ValidateNested({ each: true })
  @Type(() => CreateUserDto)
  recipients: CreateUserDto[];
}

Best Practices

  • Register a single global ValidationPipe in main.ts so no endpoint can accidentally skip validation.
  • Always enable whitelist: true (and usually forbidNonWhitelisted: true) to defend against mass-assignment attacks.
  • Turn on transform: true so handlers receive real DTO instances and correctly typed primitives.
  • Keep validation rules on the DTO with class-validator decorators rather than scattering manual checks through controllers.
  • Use @ValidateNested() plus @Type() for nested objects and arrays — without @Type() the children are never validated.
  • Set disableErrorMessages: true in production if your validation messages might reveal sensitive schema details.
Last updated June 14, 2026
Was this helpful?