A DTO is a class that describes one request body or query string. ValidationPipe builds an instance of it from the plain object that arrived with class-transformer, runs the class-validator decorators on that instance, and either hands the typed object to your method or throws. It is the validate({ body: schema }) middleware of Validation Middleware, minus the wiring.
export class CreateBookDto {
@ApiProperty({ example: 'Solaris' }) @IsString() @Length(1, 200) title!: string;
@Matches(/^au_\d+$/, { message: 'authorId must look like au_1' }) authorId!: string;
@IsInt() @Min(1450) @Max(2100) year!: number;
@IsIn(['fantasy', 'sci-fi', 'history', 'other']) genre!: string; }
export class ListBooksDto { // the query string: defaults, bounds, an allow-list
@IsOptional() @IsInt() @Min(1) @Max(100) limit: number = 20;
@IsOptional() @IsIn(['title', '-title', 'year', '-year']) sort?: string; }
export class UpdateBookDto extends PartialType(CreateBookDto) {} // every field optional
app.useGlobalPipes(new ValidationPipe({ // src/bootstrap.ts, once for the application
whitelist: true, forbidNonWhitelisted: true, transform: true, errorHttpStatusCode: 422,
transformOptions: { enableImplicitConversion: true } })); // "2" in a query -> 2PartialType from @nestjs/swagger copies the class and makes every property optional, keeping both the validators and the OpenAPI metadata — the newBook.partial() of the Zod 44,027 version. Of the pipe's options, whitelist strips undeclared fields and forbidNonWhitelisted refuses the request instead, which is Zod's .strict(); transform hands the controller a real DTO instance. And enableImplicitConversion is not optional in practice: a query string is all strings, so ?limit=2 fails @IsInt() without it.
$ curl -s "localhost:4310/api/v1/books?limit=999&sort=pages" # 422
{"error":{"code":"validation_failed","message":"Request validation failed","details":
["limit must not be greater than 100","sort must be one of the following values: title,
-title, year, -year"]},"requestId":"95c522f6"}
$ curl -s -X POST .../books -d '{"title":"","authorId":"3","year":"soon","isbn":"x"}' # 422
{"error":{...,"details":["property isbn should not exist","title must be longer than or equal
to 1 characters","authorId must look like au_1","year must be an integer"]},...}Like Zod, the pipe reports every problem, not the first. Note that year: "soon" also trips the range checks: class-validator runs each decorator independently, while Zod stops at the type failure.