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.
| Decorator | Checks |
|---|---|
@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. Treatwhitelist: trueas 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
ValidationPipeinmain.tsinstead of decorating every controller. - Always enable
whitelist: trueto strip undeclared fields; addforbidNonWhitelistedto reject them. - Enable
transform: trueso 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-validatorreads runtime metadata that interfaces cannot provide.