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 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.
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);
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 PartialType(CreateDto)
: an update by its nature concerns a subset of fields.UpdateDto
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.
creates a DTO without the selected fields, and OmitType
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 PickType
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.This lesson closes the module, so it is worth seeing the whole. Building a validation system has five stages, in this order:
class-validator and class-transformer.ValidationPipe.@Body().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
- not 400 Bad Request
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.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
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 transform: true
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
→ implement @ValidatorConstraint
and validate()
→ create a decorator function with defaultMessage()
→ use registerDecorator()
in a DTO.@IsMyValidator()
Let us end by looking wider. Validation is one of an API's layers of protection, and the lines of defence run:
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.
A different gate, different rules, @name:
PartialType(CreateDto) creates a copy of the DTO with all fields optional and is the commonest way to an UpdateDto,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,class-validator and class-transformer → DTO classes with decorators → the global ValidationPipe → DTOs in controllers with @Body() → custom validators,import { → IsString, IsNumber, IsNotEmpty → } → from 'class-validator';,ValidationPipe → the controller → the global pipe,400 Bad Request,@UsePipes( → new ValidationPipe( → { whitelist: true }) → ),transform: true converts URL parameters from strings to the right types,@Type( → () => → WeaponDto → ) for nested validation,@ValidatorConstraint → validate() and defaultMessage() → a function with registerDecorator() → using @IsMyValidator(),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.