NestJS course Β· Module 10: Data Validation
Validation groups - different rules for different gates
In this lesson6
The same province, two different gates. On the way in the guard checks everything: papers, cargo, seals. On the way out he asks only about what has changed. The rules differ, though the goods are the same.
Your DTO has exactly that problem. Creating a centurion demands a name, a rank and a cohort. Updating one, if it demands the same fields, forces the client to send the lot merely to change a single number. This lesson shows two ways out - and gathers the whole module together.
Validation groups
Validation groups are used for applying different validation rules in different contexts - one set on creation, another on update, for instance. Not for grouping decorators into separate files, not for creating user groups with permissions, and not for organising unit tests.
They are written on the decorators and activated on the pipe:
1export class CenturionDto {
2 @IsNotEmpty({ groups: ['create'] })
3 @IsOptional({ groups: ['update'] })
4 @IsString({ groups: ['create', 'update'] })
5 name: string;
6
7 @IsNumber({ groups: ['create', 'update'] })
8 cohortId: number;
9}
10
11@Post()
12@UsePipes(new ValidationPipe({ groups: ['create'] }))
13create(@Body() dto: CenturionDto) {}One class serves two contexts, because each decorator knows which group it holds in. The drawback is legibility: with ten fields and three groups it takes careful reading to say what exactly is required on an update.
Which is why in practice the second solution is reached for more often.
Mapped types
Instead of one class with groups - two classes, the second derived from the first. The helpers of the @nestjs/mapped-types package do this:
1export class CreateCenturionDto {
2 @IsString()
3 @IsNotEmpty()
4 name: string;
5
6 @IsNumber()
7 cohortId: number;
8
9 @IsString()
10 password: string;
11}
12
13class UpdateDto extends PartialType(CreateDto) {}
14
15const PublicUserDto = OmitType(CreateUserDto, ['password']);
16const NameOnlyDto = PickType(CreateCenturionDto, ['name']);
17const FullDto = IntersectionType(CreateCenturionDto, MetadataDto);PartialType(CreateDto) creates a copy of the DTO with all fields optional - it is what makes all the fields optional. It does not split the DTO into smaller parts, does not remove half the fields, and does not merge two DTOs into one. That is precisely why it is the one most commonly used to create an UpdateDto: an update by its nature concerns a subset of fields.
The inheritance is written in a fixed order: class UpdateDto β extends β PartialType(CreateDto) β {}. The class body usually stays empty, because everything arrives from the base class along with its decorators.
The remaining three divide the work between them. OmitType creates a DTO without the selected fields, and PickType a DTO with only the selected fields; that is the sole difference between them, not speed and not a destination of REST versus GraphQL. An OmitType call is written in the order: OmitType( β CreateUserDto β , ['password'] β ). IntersectionType merges two DTOs into one.
Choose by whichever is fewer. When you want to hide one field out of twenty - OmitType. When you want to keep two - PickType.
The whole validation system
This lesson closes the module, so it is worth seeing the whole. Building a validation system has five stages, in this order:
- Install
class-validatorandclass-transformer. - Create DTO classes with decorators.
- Configure the global
ValidationPipe. - Use the DTOs in controllers with
@Body(). - Add custom validators (optional).
The decorators are imported in the order: import { β IsString, IsNumber, IsNotEmpty β } β from 'class-validator';.
The layers themselves run from the lowest: decorators on DTO fields (@IsString, @IsNumber) β ValidationPipe, which runs them β the controller, where the DTO reaches @Body β the global pipe through app.useGlobalPipes. Each higher layer covers a wider scope: a decorator concerns a field, a pipe one argument, a controller a group of endpoints, a global pipe the whole application.
When validation fails, NestJS returns 400 Bad Request - not 200, not 401 and not 500. It is an answer from the "client's fault" class: the data was faulty and the server acted correctly in rejecting it.
Options and decorators easily forgotten
A pipe can be attached to a single endpoint, and the notation has a fixed order: @UsePipes( β new ValidationPipe( β { whitelist: true }) β ). The two closings at the end are no mistake - one closes the pipe's constructor, the other the decorator itself.
The transform: true option automatically converts URL parameters from strings to the appropriate types. It does not change the response format to XML, does not turn validation errors into logs, and certainly does not disable class-transformer transformation - it does the exact opposite. Without it @Query('page') page: number gives you the string '2', though the type says number.
Nested objects call for @Type, whose notation reads in the order: @Type( β () => β WeaponDto β ). The arrow function inside is not ornament - it defers evaluating the class, which is what makes it work even when two classes refer to one another.
Creating a custom validation decorator has four steps: create a class with @ValidatorConstraint β implement validate() and defaultMessage() β create a decorator function with registerDecorator() β use @IsMyValidator() in a DTO.
Validation among the other defences
Let us end by looking wider. Validation is one of an API's layers of protection, and the lines of defence run:
- CORS - request origin control. It settles whether a browser at a given address may speak at all.
- Guards - authorization and authentication. They settle who the caller is.
- ValidationPipe - input data validation. It settles whether the data sent makes sense.
- Business logic - the logic in the service. It settles the rest.
That order has a practical consequence worth remembering: a request without permissions is rejected before anybody inspects its content. A client with no token therefore cannot learn from an error message which fields an endpoint requires when it has no access to that endpoint at all.
Summary
A different gate, different rules:
- validation groups apply different rules in different contexts (create vs update) - they do not group files, users or tests,
PartialType(CreateDto)creates a copy of the DTO with all fields optional and is the commonest way to anUpdateDto,- the notation:
class UpdateDtoβextendsβPartialType(CreateDto)β{}, OmitTypecreates a DTO without the selected fields,PickTypeone with only those selected;OmitType(βCreateUserDtoβ, ['password']β),IntersectionTypemerges two DTOs into one,- building the system: install
class-validatorandclass-transformerβ DTO classes with decorators β the globalValidationPipeβ DTOs in controllers with@Body()β custom validators, - the import:
import {βIsString, IsNumber, IsNotEmptyβ}βfrom 'class-validator';, - validation layers from the lowest: decorators on DTO fields β
ValidationPipeβ the controller β the global pipe, - on failed validation NestJS returns
400 Bad Request, @UsePipes(βnew ValidationPipe(β{ whitelist: true })β),transform: trueconverts URL parameters from strings to the right types,@Type(β() =>βWeaponDtoβ)for nested validation,- a custom validator in four steps: a class with
@ValidatorConstraintβvalidate()anddefaultMessage()β a function withregisterDecorator()β using@IsMyValidator(), - the API's lines of defence: CORS β Guards β ValidationPipe β business logic.
In the next lesson we gather all of it into a project. For now remember: validation is not a matter of adding as many decorators as possible. It is a matter of having, in each context, the rules that make sense there - and no others.
Code for this lesson: src/validation-groups.ts
1// Validation groups - Different rules for different gates
2import {
3 IsString,
4 IsNotEmpty,
5 IsNumber,
6 Min,
7 Max,
8 IsOptional,
9 IsEnum,
10} from 'class-validator';
11
12enum Rank {
13 MILES = 'miles',
14 CENTURIO = 'centurio',
15 LEGATUS = 'legatus',
16}
17
18// TODO: Add validation groups to decorators
19export class LegionaryDto {
20 // TODO: Required for 'create', optional for 'update'
21 @IsString()
22 @IsNotEmpty()
23 name: string;
24
25 // TODO: Required for 'create', optional for 'update'
26 @IsEnum(Rank)
27 rank: Rank;
28
29 // TODO: Required for 'create', optional for 'update'
30 @IsNumber()
31 @Min(16)
32 @Max(65)
33 age: number;
34}
35
36// Alternative: PartialType from @nestjs/mapped-types
37// import { PartialType } from '@nestjs/mapped-types';
38
39class CreateLegionaryDto {
40 @IsString()
41 @IsNotEmpty()
42 name: string;
43
44 @IsEnum(Rank)
45 rank: Rank;
46
47 @IsNumber()
48 @Min(16)
49 age: number;
50}
51
52// TODO: Create UpdateLegionaryDto
53// Use the pattern: all fields with @IsOptional()
54class UpdateLegionaryDto {
55 @IsOptional()
56 @IsString()
57 name?: string;
58
59 // Add remaining fields as optional
60}
61
62console.log('Validation groups configured!');
63console.log('Create: everything required');
64console.log('Update: fields optional');
65Spotted a mistake in this lesson?
Check yourself
Answer the questions from this lesson. Pick an answer to see right away whether it is correct.
1. What are validation groups used for in class-validator?
2. Which mapped type in NestJS makes ALL DTO fields optional?
These are 2 of 7 questions for this lesson. Solve the rest in the game.
Hands-on tasks in the game
- Code editor
Complete UpdateCenturionDto by adding @IsOptional() and appropriate validators to each field
- Horizontal ordering
Arrange the elements of the UpdateDto definition using PartialType:
- Click in order
Arrange the OmitType call to create a public DTO without the 'password' field:
- Vertical ordering
Arrange the stages of building a complete validation system in NestJS:
- Code editor
Complete the validation decorators and controller in a full NestJS module
- Horizontal ordering
Arrange the elements of using @UsePipes on an endpoint in the correct order:
- Click in order
Arrange the elements of importing decorators from class-validator:
- Vertical ordering
Arrange the validation layers from the lowest to the highest level:
- Vertical ordering
Arrange the API protection layers from the first to the last line of defense:
- Horizontal ordering
Arrange the elements of the @Type decorator for nested validation:
- Click in order
Arrange the steps for creating a custom validation decorator: