Files
What you'll learn
- Describe a request body with a DTO class and class-validator rules, and read the messages the rules generate
- Enable ValidationPipe once for the whole application, with whitelist stripping unknown fields
- Convert a route parameter with ParseIntPipe and get a 400 for input that is not a number
Pipes and Validation
So far every handler has trusted its input. POST /cats with { "name": 42 } stores a cat named 42, and GET /cats/abc calls findOne(NaN). A real API checks its input before any logic runs, and in Nest that is the job of a pipe: something that runs on a handler's arguments just before the handler. A pipe does one of two things. It transforms the value (a string from the URL into a number), or it validates it and throws if it is wrong. Whatever a pipe throws becomes an error response before your code runs, which is what keeps handlers free of checks.
Transforming a route parameter
Route parameters arrive as strings. ParseIntPipe turns one into a number, or rejects the request:
@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number) { ... }
The pipe goes in the decorator, after the key, and the parameter's type becomes number because that is what arrives. GET /orders/abc never reaches the handler:
{ "message": "Validation failed (numeric string is expected)", "error": "Bad Request", "statusCode": 400 }
Nest ships ParseIntPipe, ParseFloatPipe, ParseBoolPipe, ParseUUIDPipe, ParseEnumPipe, ParseArrayPipe and DefaultValuePipe, and any of them works on @Param(), @Query() or @Body(). @Query('page', new DefaultValuePipe(1), ParseIntPipe) is the usual way to read an optional numeric query value: pipes run left to right.
Describing a body with a DTO
A DTO (data transfer object) is a class that describes what a request body must look like. It is a class rather than an interface for the reason from lesson 3: interfaces vanish at compile time, and validation runs at runtime. The class carries decorators from class-validator, one per rule:
import { IsEmail, IsIn, IsOptional, MinLength } from 'class-validator';
export class CreateUserDto {
@IsEmail()
email: string;
@MinLength(8)
password: string;
@IsOptional()
@IsIn(['admin', 'reader'])
role?: string;
}
Rules stack on one property. Each failing rule produces one message, and the messages are what the client receives. The ones you will reach for most:
| Decorator | Rule |
|---|---|
@IsString(), @IsInt(), @IsNumber(), @IsBoolean() | the value has that type |
@Length(min, max), @MinLength(n), @MaxLength(n) | string length |
@Min(n), @Max(n), @IsPositive() | numeric range |
@IsEmail(), @IsUrl(), @IsUUID(), @IsDateString() | a string of that format |
@IsIn([...]), @IsEnum(E) | one of a set of values |
@IsOptional() | the other rules are skipped when the value is missing |
@IsArray(), @ArrayMaxSize(n), @ValidateNested() | arrays and nested objects |
Turning validation on
Decorators on a DTO do nothing by themselves. ValidationPipe reads them and validates every argument whose declared type is a decorated class. Enable it once, for the whole application, with app.useGlobalPipes() in main.ts, and every @Body() typed with a DTO is checked from then on.
An invalid body is answered with a 400 and one message per failed rule, ordered by the property's position in the class:
{
"message": ["email must be an email", "password must be longer than or equal to 8 characters"],
"error": "Bad Request",
"statusCode": 400
}
The messages are generated from the rule and the property name, which is why the property names in a DTO are part of the API.
Three options matter from the first day. whitelist: true removes properties the DTO does not declare, so a body with an extra isAdmin field arrives without it, which closes a whole class of mass-assignment bugs. forbidNonWhitelisted: true rejects such bodies instead of trimming them. transform: true converts primitive types ("3" to 3 where the DTO says number) and hands the handler a real instance of the DTO class rather than a plain object.
Where it sits
Pipes run last before the handler, after middleware, guards and interceptors, which you will meet in the next lessons. By the time a pipe runs, the request has been allowed through; a pipe's only question is whether the arguments are usable.
Your task
- Turn validation on for the whole application in
main.ts, with unknown properties stripped. - Give
CreateCatDtoits rules:nameis a string of 1 to 30 characters, andageis an integer of at least 0. findOnestill readsidas a string and converts it by hand. Let a pipe do it, and make bad input a 400.
The tests send a body with an unknown color field, an invalid body, and a non-numeric id. Watch which of the three each change fixes. The expected error messages are in the Endpoints tab, and they are generated from the rules, so a rule that means the same thing but says it differently will not pass.
When it fails
- The invalid body is accepted: either
ValidationPipeis not enabled, or the DTO has no decorators, or the handler's parameter is not typed with the DTO class. colorcomes back in the response: unknown properties are not being stripped. Check the pipe's options.- The messages differ from the expected ones: a different rule was used.
@IsNotEmpty()says "should not be empty",@Length(1, 30)says "must be longer than or equal to 1 characters". Match the meaning in the task. GET /cats/abcstill reaches the handler: the parameter has no pipe.
Remember
- A pipe transforms or validates an argument before the handler runs; what it throws becomes the response.
- A DTO is a class because validation happens at runtime.
ValidationPipeis global, enabled once;whiteliststrips what the DTO does not declare.- Error messages come from the rules, so the DTO is part of the API.
Stuck? Show a hint
main.ts: app.useGlobalPipes() with a ValidationPipe whose options strip unknown properties. The DTO: name needs a string rule and a length rule with a minimum and a maximum; age needs an integer rule and a minimum. The controller: pass ParseIntPipe as the second argument of @Param() and type the parameter as number.
Press Run tests to start the app. Its log appears here.Graded endpoints
A valid cat is created, and the unknown color field is stripped by whitelist before the handler sees the body
One message per failed rule, in property order, as a 400; the handler never runs
A number where a string is expected and a fraction where an integer is expected: type rules fail too
Only the valid cat was stored; the rejected bodies never reached the service
ParseIntPipe rejects a non-numeric id before the handler runs
The id arrives in the handler as a number