NestJS Validation Pipe
The ValidationPipe is a built-in NestJS pipe that automatically validates incoming request data against a DTO. It runs before your controller method executes and rejects the request with a 400 error if the data does not match the expected shape. Without it, invalid data silently reaches your service and causes unpredictable behavior.
What a Pipe Does
A pipe is a class that sits between the request and the controller method. It receives the incoming data, processes it (validates, transforms, or sanitizes it), and either passes the cleaned data to the method or throws an error that stops the request entirely.
Client Request
|
v
ValidationPipe
┌─────────────────────────────┐
│ 1. Receives raw body data │
│ 2. Compares to DTO class │
│ 3. Checks validation rules │
│ 4a. Pass → clean data sent │
│ 4b. Fail → 400 error thrown │
└─────────────────────────────┘
|
v
Controller Method (only if validation passes)
Setting Up ValidationPipe Globally
The most effective way to use ValidationPipe is to apply it globally in main.ts. This means every route in your entire application validates its inputs automatically:
// 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();
The Three Key Options
whitelist: true
Strips any properties from the request body that do not exist in the DTO. If your CreateUserDto has only name and email, and the client sends a role field, the role field is silently removed before the data reaches the controller. This prevents clients from injecting unexpected fields.
forbidNonWhitelisted: true
Goes one step further: instead of silently stripping extra fields, it throws a 400 error if the client sends any field not declared in the DTO. Combine this with whitelist: true for strict input control.
transform: true
Automatically converts request data to the types declared in the DTO. If your DTO declares age: number and the query string delivers '25' as a string, the pipe converts it to the number 25. This removes the need to manually call parseInt() in your controller.
Applying ValidationPipe to a Single Route
If you want validation only on specific routes (not globally), apply the pipe directly in the decorator:
@Post()
create(@Body(ValidationPipe) createUserDto: CreateUserDto) {
return this.usersService.create(createUserDto);
}
Validation Error Response
When validation fails, NestJS sends a structured 400 response automatically:
Request: POST /users
Body: { "name": "", "email": "not-an-email", "age": -5 }
Response (400 Bad Request):
{
"statusCode": 400,
"message": [
"name should not be empty",
"email must be an email",
"age must not be less than 0"
],
"error": "Bad Request"
}
The message array lists every validation error found. The client receives clear, actionable feedback without any error-handling code in your controller or service.
ValidationPipe With ParseIntPipe Together
Pipes can stack on a single parameter. Combine ParseIntPipe (to convert the route parameter) with the global ValidationPipe (to validate the body) in the same controller method:
@Patch(':id')
update(
@Param('id', ParseIntPipe) id: number, // ParseIntPipe converts ':id'
@Body() updateUserDto: UpdateUserDto, // ValidationPipe (global) validates body
) {
return this.usersService.update(id, updateUserDto);
}
Custom Validation Messages
The validation decorators on your DTO (from class-validator) accept a custom message option:
@IsEmail({}, { message: 'Please provide a valid email address' })
email: string;
When this validation fails, the custom message appears in the error response instead of the default one. Custom messages make error responses more user-friendly when your API serves non-technical consumers or mobile app users.
Why Global Validation Matters
Setting up ValidationPipe globally once in main.ts protects every endpoint in your application. You never forget to add it to a new route. The cost is one setup step; the benefit is automatic input safety across the entire API for its entire lifetime.
