We use cookies to enhance your experience on the site
CodeWorlds

DTOs and basic validation

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.

What are DTOs?

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}

Creating DTO classes

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!

Installing class-validator and class-transformer

To add validation, we need two libraries:

1npm install class-validator class-transformer
  • class-validator - provides validation decorators (@IsString, @IsNumber, etc.)
  • class-transformer - transforms plain JSON objects into DTO class instances

Validation decorators

Now 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 |

Update DTO - PartialType

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.

ValidationPipe - global validation

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:

  • whitelist: true - automatically removes fields not present in the DTO (like a guard rejecting suspicious packages)
  • forbidNonWhitelisted: true - instead of silently removing unknown fields, throws an error (stricter control)
  • transform: true - automatically converts data to appropriate types (string "5" to number 5)

Complete controller example with DTOs

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}

Why are DTOs important?

  1. Security - they block unwanted data before it reaches business logic
  2. Documentation - they clearly define the API contract
  3. Validation - automatic verification of data correctness
  4. Typing - TypeScript knows the data shape throughout the application
  5. Separation - they separate the transport layer from business logic

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!

Go to CodeWorlds