NestJS course Β· Module 1: NestJS Basics

DTOs and basic validation

5 min read
In this lesson8

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:

DecoratorDescription
@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 comes from a separate package, which you install with npm i @nestjs/mapped-types. It 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!

Code for this lesson: src/dto-validation.ts
1// DTOs and Validation - Protective Shields of the Imperium
2import { IsString, IsNumber, IsEmail, MinLength, MaxLength, Min, Max, IsNotEmpty, IsOptional, IsIn } from 'class-validator';
3
4console.log("DTOs and validation - gate guard of the imperium!");
5
6// ===========================================
7// 1. DTO - Data Transfer Object
8// ===========================================
9
10// DTO class defines the shape of data
11class CreateLegionaryDto {
12  // @IsString()
13  // @IsNotEmpty()
14  // @MinLength(2)
15  // @MaxLength(50)
16  name: string;
17
18  // @IsString()
19  // @IsIn(['Miles', 'Optio', 'Centurio', 'Tribunus', 'Legatus'])
20  rank: string;
21
22  // @IsString()
23  // @IsNotEmpty()
24  province: string;
25
26  // @IsEmail()
27  email: string;
28
29  // @IsNumber()
30  // @Min(0)
31  // @Max(30)
32  experience: number;
33}
34
35console.log("=== CREATE LEGIONARY DTO ===");
36console.log("name: @IsString, @IsNotEmpty, @MinLength(2)");
37console.log("rank: @IsString, @IsIn([...])");
38console.log("province: @IsString, @IsNotEmpty");
39console.log("email: @IsEmail");
40console.log("experience: @IsNumber, @Min(0), @Max(30)");
41
42// ===========================================
43// 2. Validation decorators
44// ===========================================
45
46console.log("\n=== VALIDATION DECORATORS ===");
47console.log("@IsString()     - checks if text");
48console.log("@IsNumber()     - checks if number");
49console.log("@IsEmail()      - checks if email");
50console.log("@IsNotEmpty()   - checks if not empty");
51console.log("@MinLength(n)   - minimum text length");
52console.log("@MaxLength(n)   - maximum text length");
53console.log("@Min(n)         - minimum numeric value");
54console.log("@Max(n)         - maximum numeric value");
55console.log("@IsOptional()   - optional field");
56console.log("@IsIn([...])    - value from list");
57
58// ===========================================
59// 3. Update DTO with PartialType
60// ===========================================
61
62// import { PartialType } from '@nestjs/mapped-types';
63// class UpdateLegionaryDto extends PartialType(CreateLegionaryDto) {}
64// -> All fields become optional!
65
66class UpdateLegionaryDto {
67  name?: string;
68  rank?: string;
69  province?: string;
70  email?: string;
71  experience?: number;
72}
73
74console.log("\n=== UPDATE DTO (PartialType) ===");
75console.log("PartialType makes all fields optional");
76console.log("Ideal for PATCH - partial update");
77
78// ===========================================
79// 4. ValidationPipe - global validation
80// ===========================================
81
82// W main.ts:
83// app.useGlobalPipes(new ValidationPipe({
84//   whitelist: true,             // Strips unknown fields
85//   forbidNonWhitelisted: true,  // Error on unknown fields
86//   transform: true,             // Type conversion
87// }));
88
89console.log("\n=== VALIDATION PIPE ===");
90console.log("whitelist: true -> strips fields outside DTO");
91console.log("forbidNonWhitelisted: true -> error on unknown fields");
92console.log("transform: true -> type conversion (string -> number)");
93
94// ===========================================
95// 5. Example usage in controller
96// ===========================================
97
98console.log("\n=== CONTROLLER WITH DTO ===");
99console.log("@Post()");
100console.log("create(@Body() createDto: CreateLegionaryDto) { ... }");
101console.log("");
102console.log("@Patch(':id')");
103console.log("update(@Param('id') id, @Body() updateDto: UpdateLegionaryDto) { ... }");
104
105// ===========================================
106// 6. Example validation error response
107// ===========================================
108
109const validationError = {
110  statusCode: 400,
111  message: [
112    "name must be longer than or equal to 2 characters",
113    "email must be an email",
114    "experience must not be greater than 30"
115  ],
116  error: "Bad Request"
117};
118
119console.log("\n=== VALIDATION ERROR EXAMPLE ===");
120console.log(JSON.stringify(validationError, null, 2));
121
122console.log("\n=== SUMMARY ===");
123console.log("1. DTO = data shape (API contract)");
124console.log("2. class-validator = validation decorators");
125console.log("3. class-transformer = object conversion");
126console.log("4. ValidationPipe = automatic validation");
127console.log("5. PartialType = optional fields (for PATCH)");
128

Spotted a mistake in this lesson?

Check yourself

Answer the questions from this lesson. Pick an answer to see right away whether it is correct.

  1. 1. What does the abbreviation DTO stand for in the NestJS context?

  2. 2. Which class-validator decorator checks if a value is a valid email address?

Hands-on tasks in the game

  • Vertical ordering

    Arrange the steps for adding DTO validation in NestJS:

  • Code editor

    Write a CreateLegionaryDto class with name (@IsString, @IsNotEmpty) and email (@IsEmail) fields

  • Click in order

    Arrange the syntax for activating ValidationPipe in main.ts:

  • Horizontal ordering

    Arrange the elements for creating UpdateDto with PartialType:

  • Code editor

    In main.ts, write a setupValidation(app) function that enables global validation in the application: it calls app.useGlobalPipes() with a new ValidationPipe whose whitelist and transform options are set to true.

Useful articles