Introduction

When developing backend applications, receiving data from clients is a common operation. However, we cannot always trust the data sent by clients.

For example, imagine building a Task Management API. A client sends a request to create a task:

{ "title": 12345, "description": "Learn NestJS" } 

The title should be a string, but the client has sent a number.

If our backend accepts this data without checking it, invalid information can enter our application and cause unexpected problems.

NestJS provides several features to handle this situation:

  • DTOs (Data Transfer Objects)
  • class-validator
  • class-transformer
  • Built-in Pipes
  • Custom Pipes
  • Global ValidationPipe

These features help ensure that incoming data is validated and transformed before reaching the business logic.

1. What is a DTO (Data Transfer Object)?

DTO stands for Data Transfer Object.

A DTO is a class that defines the expected structure of data transferred between the client and server.

It specifies which properties should be included in a request and their expected TypeScript types.

Example

Consider a task creation request.

Create a file called create-task.dto.ts:

export class CreateTaskDto { title!: string; description?: string; } 

Explanation:

  • title is a required string property.
  • description is an optional string property.
  • ! tells TypeScript that the property will be initialized elsewhere.
  • ? indicates that a property is optional.

When a client sends a request, the DTO describes the expected data structure.

Why do we use DTOs?

  1. Maintain a consistent request structure.
  2. Separate request data from business logic.
  3. Improve code readability and maintainability.
  4. Make validation easier.
  5. Avoid directly accepting arbitrary client data.

An important point is that TypeScript types alone do not validate incoming JSON at runtime. For runtime validation, we need validation decorators and a validation mechanism.

2. What is class-validator?

class-validator is a library used to validate class properties using decorators.

It allows developers to define rules for incoming data.

Installation

npm install class-validator class-transformer 

Example

Update create-task.dto.ts:

import { IsString, IsNotEmpty, IsOptional, MaxLength, } from 'class-validator'; export class CreateTaskDto { @IsString() @IsNotEmpty() @MaxLength(100) title!: string; @IsOptional() @IsString() @MaxLength(500) description?: string; } 

Understanding validation decorators

Decorator Purpose
@IsString() Checks whether a value is a string
@IsNotEmpty() Prevents empty values
@IsOptional() Makes a property optional
@IsBoolean() Checks whether a value is Boolean
@IsNumber() Checks whether a value is a number
@IsEmail() Validates an email address
@MinLength() Specifies minimum string length
@MaxLength() Specifies maximum string length

For example:

@IsString() title!: string; 

This tells the validation system that the title property must contain a string.

If the client sends a number instead, validation fails.

3. What is class-transformer?

class-transformer is a library that converts plain JavaScript objects into class instances and can transform values into the required types.

When a client sends JSON, the received body is initially a plain JavaScript object.

However, class-validator works with class instances and their decorator metadata.

class-transformer helps convert the incoming object into an instance of the corresponding DTO class.

For example:

import { plainToInstance } from 'class-transformer'; const requestData = { title: 'Learn NestJS', description: 'Practice validation', }; const taskDto = plainToInstance( CreateTaskDto, requestData, ); 

Now taskDto is an instance of CreateTaskDto.

Difference between class-validator and class-transformer:

  • class-validator checks whether data follows the defined rules.
  • class-transformer converts plain objects into class instances and supports data transformation.

They are commonly used together in NestJS applications.

4. What are Pipes in NestJS?

Pipes are special components in NestJS that execute before a controller method.

They are mainly used for two purposes:

  1. Validation – checking whether incoming data is valid.
  2. Transformation – converting incoming data into the required format.

For example, consider this endpoint:

@Get(':id') findOne( @Param('id', ParseIntPipe) id: number, ) { return this.tasksService.findOne(id); } 

When the client sends:

GET /tasks/10 

The URL parameter 10 initially arrives as a string.

ParseIntPipe converts it into the number 10.

If the client sends:

GET /tasks/hello 

The pipe rejects the invalid value and NestJS returns a 400 Bad Request response.

Why are Pipes useful?

  • Validate incoming parameters.
  • Convert values into appropriate types.
  • Reject invalid data before it reaches the controller.
  • Keep controllers clean and readable.

5. Types of Pipes in NestJS

NestJS provides built-in pipes and also allows developers to create custom pipes.

A. Built-in Pipes

Some commonly used built-in pipes are:

Pipe Purpose
ValidationPipe Validates incoming DTO data
ParseIntPipe Converts a value into an integer
ParseBoolPipe Converts a value into a Boolean
ParseUUIDPipe Validates UUID values
ParseFloatPipe Converts a value into a floating-point number
DefaultValuePipe Provides a default value when input is missing

Example:

@Get(':id') findOne( @Param('id', ParseIntPipe) id: number, ) { return this.tasksService.findOne(id); } 

Here, ParseIntPipe ensures that the ID is a valid integer.

B. Custom Pipes

Sometimes built-in pipes are not sufficient for application-specific requirements.

In such situations, we can create our own custom pipes.

For example, imagine that a task ID must always be a positive integer.

We can create a custom pipe:

import { PipeTransform, Injectable, BadRequestException, } from '@nestjs/common'; @Injectable() export class PositiveIntPipe implements PipeTransform { transform(value: number) { if ( !Number.isInteger(value) || value <= 0 ) { throw new BadRequestException( 'ID must be a positive integer', ); } return value; } } 

Usage:

@Get(':id') findOne( @Param('id', ParseIntPipe, PositiveIntPipe) id: number, ) { return this.tasksService.findOne(id); } 

The value first passes through ParseIntPipe, then through PositiveIntPipe.

If it is not a positive integer, the custom pipe rejects it.

Important: Custom pipes implement the PipeTransform interface and define their logic inside the transform() method.

6. What is ValidationPipe?

ValidationPipe is a built-in NestJS pipe that automatically validates incoming request data against DTO validation rules.

Instead of manually checking every property inside every controller method, we can configure validation centrally.

Global ValidationPipe Setup

Open src/main.ts:

import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { ValidationPipe } from '@nestjs/common'; async function bootstrap() { const app = await NestFactory.create(AppModule); app.useGlobalPipes( new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true, transform: true, }), ); await app.listen(process.env.PORT ?? 3000); } bootstrap(); 

This configuration applies validation to incoming requests across the application.

Understanding the configuration options

1. whitelist: true

Removes properties that do not have validation decorators in the DTO.

2. forbidNonWhitelisted: true

Instead of silently removing unexpected properties, it rejects the request with a 400 Bad Request response.

3. transform: true

Transforms incoming plain objects into DTO class instances. It also enables supported type transformations.

These options help protect the application from unexpected or invalid request data.

7. Practical Example: Create and Update Task DTOs

Let's apply these concepts to a Task Management API.

CreateTaskDto

import { IsString, IsNotEmpty, IsOptional, MaxLength, } from 'class-validator'; export class CreateTaskDto { @IsString() @IsNotEmpty() @MaxLength(100) title!: string; @IsOptional() @IsString() @MaxLength(500) description?: string; } 

UpdateTaskDto

import { IsString, IsOptional, IsBoolean, IsNotEmpty, MaxLength, } from 'class-validator'; export class UpdateTaskDto { @IsOptional() @IsString() @IsNotEmpty() @MaxLength(100) title?: string; @IsOptional() @IsString() @MaxLength(500) description?: string; @IsOptional() @IsBoolean() completed?: boolean; } 

Why are all the update properties optional?

Because a client might only want to update the completion status without changing the title or description.

For example:

{ "completed": true } 

This is a valid partial update.

8. Testing Validation Using Thunder Client

After configuring the global ValidationPipe, we can test our API using Thunder Client.

Valid Request

Method: POST

Endpoint:

http://localhost:3000/tasks 

Body:

{ "title": "Learn NestJS", "description": "Practice DTO validation" } 

Expected result:

  • Status: 201 Created
  • Task is created successfully.

Invalid Request: Wrong Data Type

{ "title": 12345, "description": "Practice NestJS" } 

Expected result:

  • Status: 400 Bad Request
  • The request is rejected because the title is not a string.

Invalid Request: Empty Title

{ "title": "", "description": "Practice validation" } 

Expected result:

  • Status: 400 Bad Request
  • The title cannot be empty.

Invalid Request: Unexpected Property

{ "title": "Learn NestJS", "description": "Practice validation", "randomField": "unexpected data" } 

Expected result:

  • Status: 400 Bad Request
  • The unexpected property is rejected because forbidNonWhitelisted is enabled.

9. Data Integrity at the Boundary

Data integrity at the boundary means validating incoming data as soon as it enters the backend application.

We should not allow invalid data to travel through the application and reach the business logic or database.

The general request flow is:

Client / Thunder Client | v HTTP Request | v ValidationPipe | v DTO Validation | v Controller | v Service | v Database 

If the incoming data is invalid, the request is rejected before reaching the controller method.

This approach improves:

  • Data consistency
  • Application security
  • Error handling
  • Code maintainability
  • Reliability of backend APIs

10. Conclusion

DTOs, Validation and Pipes are important concepts in NestJS backend development.

DTOs define the expected structure of incoming data. class-validator provides validation rules, while class-transformer supports converting plain objects into class instances.

Pipes help validate and transform incoming values before they reach controller methods. Built-in pipes handle common operations, while custom pipes allow developers to implement application-specific rules.

Finally, the global ValidationPipe makes it easier to enforce validation consistently across the application.

The main takeaway is simple: Never blindly trust client input. Validate data at the application boundary before processing it.

Key Takeaways

  • DTO = Defines the expected data structure.
  • class-validator = Validates incoming data.
  • class-transformer = Transforms plain objects into class instances.
  • Pipes = Validate and transform incoming values.
  • ValidationPipe = Automatically validates DTOs.
  • Custom Pipes = Implement custom validation and transformation logic.
  • Data Integrity = Prevents invalid data from entering the application.