Ave, gate guardian! Consul Caesar.js knows that the security of the Empire begins with controlling what enters through the gates. We cannot let invalid data into our system - that's like letting spies into the legion camp! Time to learn DTOs and validation - the protective shields of our API.
DTO (Data Transfer Object) is a class that defines the shape of data sent to and from our API. Think of a DTO as a legion recruitment form - every new legionary must fill in the appropriate fields before being accepted.
Without a DTO, our controller accepts anything - that's like an open gate without guards:
1// WITHOUT DTO - dangerous!
2@Post()
3create(@Body() body: any) {
4 // body can contain anything... dangerous!
5 return this.legionariesService.create(body);
6}With a DTO we precisely define what data we accept:
1// WITH DTO - secure like a Roman fort!
2@Post()
3create(@Body() createLegionaryDto: CreateLegionaryDto) {
4 return this.legionariesService.create(createLegionaryDto);
5}Let's create a DTO for our legionary system:
1// dto/create-legionary.dto.ts
2export class CreateLegionaryDto {
3 name: string;
4 rank: string;
5 province: string;
6 email: string;
7 experience: number;
8}However, this is only the data shape - it does not yet check whether the data is valid. This is where validation comes in!
To add validation, we need two libraries:
1npm install class-validator class-transformerNow let's add validation to our DTO:
1// dto/create-legionary.dto.ts
2import {
3 IsString, IsNumber, IsEmail,
4 MinLength, MaxLength, Min, Max,
5 IsNotEmpty, IsOptional, IsIn
6} from 'class-validator';
7
8export class CreateLegionaryDto {
9 @IsString()
10 @IsNotEmpty()
11 @MinLength(2)
12 @MaxLength(50)
13 name: string;
14
15 @IsString()
16 @IsIn(['Miles', 'Optio', 'Centurio', 'Tribunus', 'Legatus'])
17 rank: string;
18
19 @IsString()
20 @IsNotEmpty()
21 province: string;
22
23 @IsEmail()
24 email: string;
25
26 @IsNumber()
27 @Min(0)
28 @Max(30)
29 experience: number;
30}Here are the most commonly used validation decorators:
| Decorator | Description | |-----------|------| | `@IsString()` | Checks if the value is a string | | `@IsNumber()` | Checks if the value is a number | | `@IsEmail()` | Checks if the value is a valid email | | `@IsNotEmpty()` | Checks if the field is not empty | | `@MinLength(n)` | Minimum text length | | `@MaxLength(n)` | Maximum text length | | `@Min(n)` | Minimum numeric value | | `@Max(n)` | Maximum numeric value | | `@IsOptional()` | Field is optional | | `@IsIn([...])` | Value must be one of the given options |
For partial updates (PATCH) we can use `PartialType`, which makes all fields optional:
1// dto/update-legionary.dto.ts
2import { PartialType } from '@nestjs/mapped-types';
3import { CreateLegionaryDto } from './create-legionary.dto';
4
5export class UpdateLegionaryDto extends PartialType(CreateLegionaryDto) {}
6// Now all fields are optional!`PartialType` automatically creates a new class where every field from `CreateLegionaryDto` becomes optional. Thanks to this, we don't need to write a separate DTO with `@IsOptional()` on every field.
For validation to work, we need to activate `ValidationPipe` in our application:
1// main.ts
2import { NestFactory } from '@nestjs/core';
3import { ValidationPipe } from '@nestjs/common';
4import { AppModule } from './app.module';
5
6async function bootstrap() {
7 const app = await NestFactory.create(AppModule);
8
9 app.useGlobalPipes(new ValidationPipe({
10 whitelist: true, // Removes fields that are not in the DTO
11 forbidNonWhitelisted: true, // Throws error when unknown fields are present
12 transform: true, // Automatic type conversion
13 }));
14
15 await app.listen(3000);
16}
17bootstrap();Three key `ValidationPipe` options:
1@Controller('legionaries')
2export class LegionariesController {
3 constructor(private readonly legionariesService: LegionariesService) {}
4
5 @Post()
6 create(@Body() createDto: CreateLegionaryDto) {
7 return this.legionariesService.create(createDto);
8 }
9
10 @Patch(':id')
11 update(
12 @Param('id') id: string,
13 @Body() updateDto: UpdateLegionaryDto,
14 ) {
15 return this.legionariesService.update(Number(id), updateDto);
16 }
17}When someone sends invalid data, ValidationPipe will automatically return a readable error:
1{
2 "statusCode": 400,
3 "message": [
4 "name must be longer than or equal to 2 characters",
5 "email must be an email",
6 "experience must not be greater than 30"
7 ],
8 "error": "Bad Request"
9}DTOs and validation are the shield and sword of our Empire - they protect the API gates and ensure that only valid data enters the system. Every good Empire builder knows that strong walls are the foundation!