Files
What you'll learn
- Register one ValidationPipe for the application and choose whitelist, forbidNonWhitelisted and transform deliberately
- Derive an update DTO from a create DTO with PartialType so validators are written once
- Validate a body array and parse a comma-separated query list with ParseArrayPipe
Validation
Lesson 5 of Basics put a ValidationPipe on one route and watched class-validator turn a bad body into a 400. That is the mechanism. This lesson is about running it everywhere, and about the three things a real API needs from it beyond "is this field a string": dropping properties a client should not send, validating an update that carries only some fields, and validating things that are not a single object, such as a list in the body or a comma-separated list in the query string.
One pipe for the whole application
Validation belongs to every route, so it goes on the application rather than on a handler:
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe());
await app.listen(3000);
}
From here on any @Body(), @Param() or @Query() whose declared type is a class decorated with class-validator is validated before the handler runs, and a failure is a 400 with every message:
{ "statusCode": 400, "message": ["email must be an email"], "error": "Bad Request" }
disableErrorMessages: true sends only the status to the client, which production APIs sometimes prefer; the messages are for development.
Stripping and refusing unknown properties
By default a body may carry any property; the DTO only checks the ones it declares, and the rest reach your handler. Two options change that:
new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true });
whitelist removes every property that has no validation decorator, so a client cannot smuggle isAdmin: true into a DTO that never mentioned it. forbidNonWhitelisted goes further and rejects the request instead of silently cleaning it, with a message naming the property: property isAdmin should not exist. Refusing is the better default for an API with a contract; stripping is the kinder one for forms.
Transforming the payload
A request body is JSON, a route parameter is a string. transform: true makes the pipe return an instance of the DTO class rather than the raw object, and, for primitives, converts to the declared type:
new ValidationPipe({ transform: true });
@Get(':id')
findOne(@Param('id') id: number) {
console.log(typeof id); // number
}
Without transform, that id is "3" whatever the annotation says, and cats.find((c) => c.id === id) never matches. The explicit pipes still exist for when you want the conversion on one parameter only: @Param('id', ParseIntPipe) converts and rejects a non-number with Validation failed (numeric string is expected); ParseBoolPipe, ParseUUIDPipe and ParseEnumPipe do the same for their types.
Mapped types: an update is a partial create
UpdateCatDto has the same fields as CreateCatDto, each optional. Copying the class and adding ? to everything is the obvious way and the wrong one: two copies of every validator, drifting apart. @nestjs/mapped-types builds one class from another, validators included:
import { PartialType } from '@nestjs/mapped-types';
export class UpdateCatDto extends PartialType(CreateCatDto) {}
PartialType makes every property optional and keeps its decorators, so a PATCH body with only age passes, and one with age: "four" still fails the @IsInt() it inherited. PickType(CreateCatDto, ['name'] as const) keeps some properties, OmitType drops some, IntersectionType merges two classes, and they compose: PartialType(OmitType(CreateCatDto, ['name'] as const)).
Arrays
@Body() dtos: CreateCatDto[] validates nothing, because a TypeScript array type leaves no class behind at runtime for the pipe to instantiate. ParseArrayPipe takes the item class explicitly:
@Post('bulk')
createMany(@Body(new ParseArrayPipe({ items: CreateCatDto })) dtos: CreateCatDto[]) {}
Each item is validated with the class's decorators and a body that is not an array at all is Validation failed (parsable array expected). The same pipe parses a query string list, and there a bad element is named by its index, [1] item must be a number:
@Get()
findByIds(@Query('ids', new ParseArrayPipe({ items: Number, separator: ',' })) ids: number[]) {}
GET /?ids=1,2,3 arrives as three numbers. optional: true lets the parameter be absent.
Your task
The cats API gets a contract it enforces on every route.
- In
main.ts, register oneValidationPipefor the application that strips undeclared properties, refuses a request that sends one, and transforms payloads to their declared types. - Derive
UpdateCatDtofromCreateCatDtowith every property optional and every validator kept. POST /cats/bulktakes an array ofCreateCatDtoand validates each item;GET /cats?ids=1,3takes a comma-separated list of numbers.- With transformation on,
GET /cats/:idandPATCH /cats/:idcan declareidas anumberand use it directly.
Run after step 1 only and send PATCH /cats/1 with { "age": "four" }: the starter's hand-written UpdateCatDto has no decorators, so whitelist strips everything and the update does nothing, quietly. That is what PartialType fixes.
When it fails
- A body with a typo in a property name is accepted and the cat is created without it:
whitelistalone strips it. AddforbidNonWhitelistedto getproperty colour should not exist. PATCHwith valid fields answers400 property age should not exist:UpdateCatDtodeclares the property without a validator, so the whitelist treats it as unknown. Derive it withPartialType.GET /cats/3answers404for a cat that exists: the parameter is a string and the comparison is strict. Turntransformon, or useParseIntPipe.POST /cats/bulkaccepts[{ "name": "Nala" }]: the array type is erased; onlyParseArrayPipe({ items })validates the items.
Remember
app.useGlobalPipes(new ValidationPipe({ ... }))validates every route;whiteliststrips,forbidNonWhitelistedrefuses,transformconverts.PartialType,PickType,OmitTypeandIntersectionTypederive DTOs and keep the validators.ParseArrayPipe({ items })validates each element of a body array; withseparator, it parses a query-string list.ParseIntPipeand friends convert one parameter explicitly;transform: trueconverts by declared type.
Stuck? Show a hint
main.ts: app.useGlobalPipes(new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true, transform: true })). update-cat.dto.ts: extend PartialType(CreateCatDto) from @nestjs/mapped-types. Controller: @Body(new ParseArrayPipe({ items: CreateCatDto })) for the bulk body, @Query('ids', new ParseArrayPipe({ items: Number, separator: ',', optional: true })) for the list, and with transform on, @Param('id') id: number arrives as a number.
Press Run tests to start the app. Its log appears here.Graded endpoints
A valid body passes the global pipe
Every failed decorator reports its message, and the request never reaches the handler
forbidNonWhitelisted refuses a property no decorator declares, naming it
PartialType made every property optional, so a body with only age passes, and transform turned the :id into a number the service can compare
The derived DTO kept the create DTO's validators: age must still be an integer
ParseArrayPipe validated each item as a CreateCatDto
An item missing its age fails the item validation, so nothing is created
The comma-separated list arrived as numbers
ParseArrayPipe names the bad element
The :id parameter is a number, so the strict comparison in the service finds the cat
Three cats, Tom updated