We use cookies to enhance your experience on the site
CodeWorlds

Validation groups - different rules for different gates

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, @name:

  • 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.

Go to CodeWorlds