NestJS in Production · Structure, Validation and Errors
Validating Input with DTOs and ValidationPipe
Validate and transform request bodies with class-validator DTOs and a global ValidationPipe that strips unknown fields.
What you'll build
A POST /users endpoint with a CreateUserDto validated by a global ValidationPipe: wrong types and missing fields produce a 400 with messages, unknown properties are rejected, and query strings are transformed into numbers.
The idea
- A DTO class describes the expected input with class-validator decorators (
@IsEmail(),@Length(),@IsInt()). ValidationPipetransforms the plain body into a DTO instance and validates it before the handler runs.whitelist: truestrips properties without decorators;forbidNonWhitelisted: truerejects them (good for catching client bugs and mass-assignment attempts).transform: trueconverts primitive types (route and query parameters) to the declared types.
The code
import "reflect-metadata";
import { Body, Controller, Get, Module, Post, Query, ValidationPipe } from "@nestjs/common";
import { NestFactory } from "@nestjs/core";
import { IsEmail, IsInt, IsOptional, Length, Max, Min } from "class-validator";
import { Type } from "class-transformer";
class CreateUserDto {
@IsEmail() email: string;
@Length(2, 50) name: string;
}
class ListQuery {
@IsOptional() @Type(() => Number) @IsInt() @Min(1) @Max(100) limit: number = 20;
}
@Controller("users")
class UsersController {
@Post() create(@Body() dto: CreateUserDto) { return { created: dto, isDto: dto instanceof CreateUserDto }; }
@Get() list(@Query() q: ListQuery) { return { limit: q.limit, type: typeof q.limit }; }
}
@Module({ controllers: [UsersController] })
class AppModule {}
async function main() {
const app = await NestFactory.create(AppModule, { logger: false });
app.useGlobalPipes(new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true, transform: true }));
await app.listen(0, "127.0.0.1");
const base = `http://127.0.0.1:${app.getHttpServer().address().port}`;
const post = async (body: object) => {
const r = await fetch(`${base}/users`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) });
console.log(r.status, JSON.stringify(await r.json()));
};
await post({ email: "[email protected]", name: "Ana" });
await post({ email: "nope", name: "A" });
await post({ email: "[email protected]", name: "Ben", role: "admin" });
for (const q of ["?limit=5", "?limit=500"]) {
const r = await fetch(`${base}/users${q}`);
console.log(q, r.status, JSON.stringify(await r.json()));
}
await app.close();
}
main();
Output
201 {"created":{"email":"[email protected]","name":"Ana"},"isDto":true}
400 {"message":["email must be an email","name must be longer than or equal to 2 characters"],"error":"Bad Request","statusCode":400}
400 {"message":["property role should not exist"],"error":"Bad Request","statusCode":400}
?limit=5 200 {"limit":5,"type":"number"}
?limit=500 400 {"message":["limit must not be greater than 100"],"error":"Bad Request","statusCode":400}Run this on your computer — see “Get Started” for setup.
Common mistakes
- Defining DTOs as interfaces or types: validators need classes and decorators at runtime.
- Forgetting
whitelist, so extra fields likerolereach your service. - Forgetting
transform, so query parameters stay strings andlimit + 1becomes"51". - Validating nested objects without
@ValidateNested()and@Type(() => Child).
Key points
- DTO classes plus a global ValidationPipe validate every request.
- whitelist and forbidNonWhitelisted block unexpected fields.
- transform converts parameters to the declared types.
Further reading: Validation · class-validator
Practice questions
Why must NestJS DTOs be classes rather than interfaces?
Answer
Validation and transformation rely on decorators and runtime type metadata, which only exist on classes; interfaces are erased at compile time.
What do whitelist and forbidNonWhitelisted do?
Answer
whitelist strips properties without validation decorators; forbidNonWhitelisted returns a 400 when such properties are present.
What does transform: true change?
Answer
The pipe returns DTO class instances and converts primitive parameters (route, query) to their declared types, such as strings to numbers.
Open in the app to mark lessons complete, take notes and track your progress.