Validation: ValidationPipe and class-validator
Validation means checking that the client sent correct data: the title is present, it is a string, its length is within limits. In NestJS the rules are written as decorators right in the DTO, and the built-in ValidationPipe performs the check. On failure the client gets a 400 response with a list of problems, and the controller method is never called.
Installation
npm i class-validator class-transformerclass-validator provides the rule decorators; class-transformer turns plain JSON into an instance of the DTO class so that the rules can be checked.
Rules in the DTO
// src/posts/dto/create-post.dto.ts
import { IsNotEmpty, IsString, MaxLength } from 'class-validator';
export class CreatePostDto {
@IsString()
@IsNotEmpty()
@MaxLength(120)
title: string;
@IsString()
@IsNotEmpty()
content: string;
}Each field has rules: a non-empty string, and the title must also be at most 120 characters long.
Enabling ValidationPipe
The pipe is enabled for the whole application in src/main.ts:
// 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 }));
await app.listen(process.env.PORT ?? 3000);
}
bootstrap();Now every @Body() argument whose type is a class with rules is checked before the method is called. Send a post without a title:
curl -X POST http://localhost:3000/posts \
-H "Content-Type: application/json" \
-d '{"content":"Text without a title"}'The server answers with code 400 and a list of broken rules:
{
"message": [
"title must be shorter than or equal to 120 characters",
"title should not be empty",
"title must be a string"
],
"error": "Bad Request",
"statusCode": 400
}A custom error text goes into the last argument of a rule: @IsNotEmpty({ message: 'Enter a title' }).
Common rules
| Decorator | What it checks |
|---|---|
@IsString() | A string |
@IsInt() | An integer |
@IsEmail() | An email address |
@IsNotEmpty() | Not an empty string, not null and not undefined |
@MinLength(8) | A string at least 8 characters long |
@MaxLength(120) | A string at most 120 characters long |
@IsOptional() | The field may be omitted |
@IsIn(['draft', 'published']) | One of the listed values |
Pipe options
| Option | What it does |
|---|---|
whitelist: true | Removes body fields that are not in the DTO |
forbidNonWhitelisted: true | Responds with 400 instead of removing them |
transform: true | Passes a DTO class instance to the method and converts parameter types |
whitelist protects against extra fields: if the client sends "role":"admin", that field never reaches the service.
Common mistake
Rules do not fire if the argument type is an interface or any: the pipe has nothing to compare the data with. The same happens if you forget app.useGlobalPipes(...) — a request with an empty body quietly reaches the service. Test validation with a deliberately bad request right after enabling it.