Stop Manual Payload Checks: ValidationPipe as a NestJS Contract
Use NestJS ValidationPipe with class-validator to enforce request contracts at the edge, transform payloads to DTOs, strip unknown fields, and return structured validation errors without manual checks in controllers.
25 Nov 2025, 17:59 UTC

Payloads arrive with extra fields, wrong types, and missing data. Manually checking every property in a controller turns handlers into validation boilerplate and lets bad data slip through to services.
A practical engineering decision in NestJS is to treat the request DTO as a contract and enforce it at the edge with ValidationPipe plus class-validator. The pipe runs before the handler, transforms plain JSON into class instances, strips unknown properties, and throws a BadRequestException with a structured error array when the contract is violated.
Global pipe setup as a single enforcement point
Enable the pipe once in main.ts so every route benefits without repeating code in controllers. The pipe needs class-validator and class-transformer installed in the project root and TypeScript metadata emission enabled.
npm i class-validator class-transformerRun in the project root with npm install permissions. Verify tsconfig.json contains:
"compilerOptions": {
"emitDecoratorMetadata": true,
"experimentalDecorators": true
}Without metadata the pipe cannot infer types for transformation.
// src/main.ts
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();whitelist: true removes properties not defined on the DTO. forbidNonWhitelisted: true rejects the request instead of silently stripping. transform: true converts payloads to class instances so type coercion and nested validation work.
DTOs as declarative contracts
Define validation rules with class-validator decorators on a plain class. The class is the source of truth for shape and constraints.
// src/users/dto/create-user.dto.ts
import { IsEmail, IsString, MinLength } from 'class-validator';
export class CreateUserDto {
@IsString()
name: string;
@IsEmail()
email: string;
@IsString()
@MinLength(8)
password: string;
}Use @Expose() from class-transformer when you need explicit transformation metadata for nested objects or custom naming.
Controller usage stays clean.
// src/users/users.controller.ts
import { Body, Controller, Post } from '@nestjs/common';
import { CreateUserDto } from './dto/create-user.dto';
@Controller('users')
export class UsersController {
@Post()
create(@Body() dto: CreateUserDto) {
// dto is already validated and transformed
return { created: true, user: dto };
}
}When validation fails NestJS throws a BadRequestException with an error array describing each violation. You can shape the response with a ValidationExceptionFilter if you need a different format.
Trade-off and limitation
ValidationPipe adds reflection and decorator processing on every request. For high-throughput endpoints this overhead is measurable. Options are to apply the pipe selectively to specific routes or controllers instead of globally, or use a lighter custom pipe for hot paths.
Strict whitelisting also breaks clients that send extra fields. With forbidNonWhitelisted: true an unknown property causes a 400 rejection rather than silent removal. Decide per API whether to reject or strip, and communicate the contract clearly.
Check the behavior by running the app locally and sending requests to POST /users. A payload matching the DTO should be accepted. A payload missing email or with a password shorter than 8 characters should return 400 with a JSON array of validation errors. Toggling whitelist and forbidNonWhitelisted lets you observe stripping versus rejection.
Use ValidationPipe as the edge contract, keep DTOs small and explicit, and reserve manual checks for business rules that cannot be expressed declaratively.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.