NestJS course Β· Module 10: Data Validation

Validation groups - different rules for different gates

7 min read
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:

  1. Install class-validator and class-transformer.
  2. Create DTO classes with decorators.
  3. Configure the global ValidationPipe.
  4. Use the DTOs in controllers with @Body().
  5. 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:

  1. CORS - request origin control. It settles whether a browser at a given address may speak at all.
  2. Guards - authorization and authentication. They settle who the caller is.
  3. ValidationPipe - input data validation. It settles whether the data sent makes sense.
  4. 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 an UpdateDto,
  • the notation: class UpdateDto β†’ extends β†’ PartialType(CreateDto) β†’ {},
  • OmitType creates a DTO without the selected fields, PickType one with only those selected; OmitType( β†’ CreateUserDto β†’ , ['password'] β†’ ),
  • IntersectionType merges two DTOs into one,
  • building the system: install class-validator and class-transformer β†’ DTO classes with decorators β†’ the global ValidationPipe β†’ 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: true converts URL parameters from strings to the right types,
  • @Type( β†’ () => β†’ WeaponDto β†’ ) for nested validation,
  • a custom validator in four steps: a class with @ValidatorConstraint β†’ validate() and defaultMessage() β†’ a function with registerDecorator() β†’ 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');
65

Spotted 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. 1. What are validation groups used for in class-validator?

  2. 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:

Useful articles