We use cookies to enhance your experience on the site
CodeWorlds

Validation Groups — Different Rules for Different Gates

In the Roman Empire, not every gate has the same rules. The trade gate lets merchants through, the military gate — legionaries, and the senate gate — only senators. In class-validator, we have a similar mechanism: validation groups.

The Problem — create vs update

A typical case: when creating a new legionary, all fields are required, but when updating, we want to allow sending only selected fields:

1// CREATE: everything required
2// { name: "Marcus", rank: "miles", age: 25, legio: "X" }
3
4// UPDATE: only changed fields
5// { rank: "centurio" }  ← promotion only

Defining Groups

Each validation decorator accepts a

groups
option:

1import {
2  IsString,
3  IsNotEmpty,
4  IsNumber,
5  Min,
6  IsOptional,
7} from 'class-validator';
8
9export class LegionaryDto {
10  @IsString({ groups: ['create', 'update'] })
11  @IsNotEmpty({ groups: ['create'] })  // Required only when creating
12  name: string;
13
14  @IsString({ groups: ['create', 'update'] })
15  @IsNotEmpty({ groups: ['create'] })
16  rank: string;
17
18  @IsNumber({}, { groups: ['create', 'update'] })
19  @Min(16, { groups: ['create', 'update'] })
20  @IsNotEmpty({ groups: ['create'] })
21  age: number;
22
23  @IsString({ groups: ['create', 'update'] })
24  @IsNotEmpty({ groups: ['create'] })
25  legio: string;
26}

Activating Groups in ValidationPipe

For groups to work, you need to activate them in ValidationPipe:

1// Creation endpoint — activate the 'create' group
2@Post()
3@UsePipes(new ValidationPipe({ groups: ['create'] }))
4createLegionary(@Body() dto: LegionaryDto) {
5  return this.service.create(dto);
6}
7
8// Update endpoint — activate the 'update' group
9@Patch(':id')
10@UsePipes(new ValidationPipe({ groups: ['update'] }))
11updateLegionary(
12  @Param('id') id: string,
13  @Body() dto: LegionaryDto,
14) {
15  return this.service.update(id, dto);
16}

@IsOptional() with Groups

A common pattern — a field that's optional only during updates:

1export class LegionaryDto {
2  @IsOptional({ groups: ['update'] })
3  @IsString({ groups: ['create', 'update'] })
4  @IsNotEmpty({ groups: ['create'] })
5  name: string;
6}

During

create
, the
name
field is required and must be a string. During
update
, the
name
field is optional, but if provided — it must be a string.

Practical Pattern — Separate DTO Classes

In practice, many developers use separate DTO classes with inheritance instead of groups:

1// Base DTO with common decorators
2export class CreateLegionaryDto {
3  @IsString()
4  @IsNotEmpty()
5  name: string;
6
7  @IsString()
8  @IsNotEmpty()
9  rank: string;
10
11  @IsNumber()
12  @Min(16)
13  age: number;
14}
15
16// Update DTO - all fields optional
17export class UpdateLegionaryDto {
18  @IsOptional()
19  @IsString()
20  name?: string;
21
22  @IsOptional()
23  @IsString()
24  rank?: string;
25
26  @IsOptional()
27  @IsNumber()
28  @Min(16)
29  age?: number;
30}

NestJS also offers a special helper

PartialType
:

1import { PartialType } from '@nestjs/mapped-types';
2
3export class UpdateLegionaryDto extends PartialType(CreateLegionaryDto) {}
4// Automatically creates a copy with all fields optional!

Mapped Types — Advanced DTO Creation

NestJS offers several helpers for creating DTOs based on existing ones:

1import { PartialType, PickType, OmitType, IntersectionType } from '@nestjs/mapped-types';
2
3// All fields optional
4class UpdateDto extends PartialType(CreateDto) {}
5
6// Only selected fields
7class LoginDto extends PickType(CreateDto, ['email', 'password']) {}
8
9// Without selected fields
10class PublicDto extends OmitType(CreateDto, ['password', 'secret']) {}
11
12// Combination of two DTOs
13class FullDto extends IntersectionType(CreateDto, MetadataDto) {}

Validation groups and mapped types give you flexibility in handling different scenarios. Each gate of the empire can now have its own customized rules!

Go to CodeWorlds