Validation with ValidationPipe: Never Trust Raw Input Again
In my five years building production systems, I've watched countless bugs slip through because someone forgot to validate incoming data. Validation with ValidationPipe is NestJS's answer to that problem—a single global pipe that catches malformed, unauthorized, or unexpected payloads before they reach your business logic. Once you enable it properly, you can stop writing defensive checks and start trusting your DTOs.
Why Global Validation Matters
Consider a real scenario: you're building an e-commerce platform. A controller accepts a CreateProductDTO that should have name, price, and description. Without validation, a client sends {name: "Widget", price: "not-a-number", extra_field: "malicious", description: null}. Your service tries to save it, casting fails silently, or worse—the extra field gets stored and causes a security issue later.
Global validation catches this at the boundary. You define rules once in your DTO, apply the pipe globally, and every incoming request is validated before it reaches a handler. No surprises, no defensive coding scattered across your codebase.
Whitelist: Silently Stripping Unknown Properties
The whitelist: true option is your first line of defense. It removes any property from the incoming payload that isn't defined in your DTO.
Here's a real-world example from a CRM system I built:
import { IsString, IsEmail, MinLength } from 'class-validator';
export class CreateContactDTO {
@IsString()
@MinLength(2)
firstName: string;
@IsEmail()
email: string;
@IsString()
phone: string;
}Without whitelist enabled, if a client sends:
{
"firstName": "John",
"email": "john@example.com",
"phone": "+1234567890",
"isAdmin": true,
"internalNotes": "Mark as VIP"
}Your DTO would accept isAdmin and internalNotes even though they're not properties. When you serialize and save to the database, these extra fields might get stored or cause unexpected behavior.
With whitelist: true, NestJS silently discards isAdmin and internalNotes before validation runs. The payload becomes:
{
"firstName": "John",
"email": "john@example.com",
"phone": "+1234567890"
}This is convenient when you want to ignore unexpected fields without rejecting the entire request. But there's a security trade-off: the client doesn't know their extra fields were ignored.
forbidNonWhitelisted: Reject Unknown Properties Explicitly
Sometimes you want to be stricter. The forbidNonWhitelisted: true option tells NestJS to reject the entire request if any unknown properties are present. This is better for APIs where you want to catch client mistakes immediately.
With both options enabled together:
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
})
);Now sending that same payload with isAdmin and internalNotes triggers a validation error:
{
"statusCode": 400,
"message": [
"property isAdmin should not exist",
"property internalNotes should not exist"
],
"error": "Bad Request"
}The client sees immediately that they've sent unexpected fields. This is especially valuable in strict API contracts where you want to enforce schema discipline. I use this in trading platforms and SaaS dashboards where the API contract is critical.
Transform: Converting Payloads into DTO Instances
Here's where ValidationPipe becomes truly powerful. The transform: true option doesn't just validate—it converts your raw payload into actual DTO instances with proper types.
Let's say you're building a ride-hailing service and need to accept ride parameters:
import { Type } from 'class-transformer';
import { IsNumber, IsPositive, Min, Max } from 'class-validator';
export class BookRideDTO {
@Type(() => Number)
@IsNumber()
@IsPositive()
pickupLat: number;
@Type(() => Number)
@IsNumber()
@IsPositive()
pickupLng: number;
@Type(() => Number)
@IsNumber()
@Min(1)
@Max(6)
passengerCount: number;
}Without transform: true, the ValidationPipe validates but doesn't convert. Your handler receives strings:
@Post('book')
bookRide(@Body() dto: BookRideDTO) {
console.log(typeof dto.pickupLat); // string, not number!
console.log(dto.passengerCount); // "3" not 3
}With transform: true, the pipe uses the @Type(() => Number) decorator to convert them:
@Post('book')
bookRide(@Body() dto: BookRideDTO) {
console.log(typeof dto.pickupLat); // number
console.log(dto.passengerCount); // 3 (number)
// Now you can safely do math: distance = Math.sqrt(...)
}This is invaluable. You avoid scattered parseFloat() and parseInt() calls throughout your handlers. Your business logic works with properly typed objects from the start.
Enable Globally in main.ts
The pattern I use in every NestJS project is to enable ValidationPipe globally with all three options:
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();This single block—placed before app.listen()—protects every route in your application. Every POST, PATCH, and PUT payload gets validated, cleaned, transformed, and converted before reaching your handlers.
In a large travel booking platform I worked on, this global setup prevented dozens of type-related bugs and eliminated an entire category of defensive coding. Instead of writing:
const checkInDate = new Date(body.checkInDate);
if (isNaN(checkInDate.getTime())) {
throw new BadRequestException('Invalid date');
}
const nights = parseInt(body.nights, 10);
if (isNaN(nights) || nights < 1) {
throw new BadRequestException('Nights must be a positive number');
}You define it once in your DTO and trust the validation pipe to do the work.
Common Mistakes
Forgetting to Use class-validator Decorators
ValidationPipe won't magically validate a DTO without decorators. This doesn't work:
export class CreateUserDTO {
email: string; // No @IsEmail()
age: number; // No @IsNumber()
}Any payload passes through untouched. Always add decorators from the class-validator package.
Mixing whitelist and forbidNonWhitelisted Inconsistently
If you enable forbidNonWhitelisted: true but forget whitelist: true, the behavior is undefined. Always use both together for predictable results.
Not Using @Type() for Transformations
Relying on transform: true alone without @Type(() => Number) won't convert string numbers to actual numbers. The decorator is what tells the transformer how to convert.
Applying ValidationPipe Only to Specific Routes
I've seen teams apply ValidationPipe to individual controllers and forget about others. Always enable it globally. If you need custom behavior for a specific route, override it there—but global is the safe default.
Key Takeaways
- whitelist: true silently removes unknown properties—useful for lenient APIs.
- forbidNonWhitelisted: true rejects requests with unknown properties—better for strict contracts.
- transform: true converts payloads into properly typed DTO instances using
@Type()decorators. - Enable all three globally in
main.tsbeforeapp.listen()to protect your entire application. - Always pair
transform: truewith@Type()decorators on your DTO properties. - Global validation eliminates defensive type-checking scattered across handlers and service methods.
- When you don't need transformation, you can disable it per-pipe to save microseconds on simple validations.






Comments
Loading comments…