DEV Community

koushikmaya
koushikmaya

Posted on

# NestJS Validation

DTO pattern, class-validator, class-transformer, built-in Pipes,
custom Pipes, and global ValidationPipe setup

These notes explain how NestJS receives data from a request, checks
whether that data is valid, and converts it into the format your
application expects.

The examples use a simple API for creating and fetching posts. You can
adapt the same ideas to users, profiles, authentication, or other
modules.


1. Why do we need validation?

Imagine your API has an endpoint that creates a post. It expects a
request body like this:

{
  "content": "Learning NestJS",
  "visibility": "public"
}
Enter fullscreen mode Exit fullscreen mode

But a client might accidentally send:

{
  "content": 12345,
  "visibility": "everyone"
}
Enter fullscreen mode Exit fullscreen mode

Without validation, incorrect data may travel deeper into your
application and cause unexpected behaviour or database errors.

Validation helps us check incoming data at the API boundary. We can
reject invalid requests early and return a clear error response.

The basic idea: Do not blindly trust data coming from the client.
Validate it before using it.


2. What is the DTO pattern?

DTO stands for Data Transfer Object.

A DTO describes the shape of data that is allowed to move into or out of
an application layer. In NestJS, DTO classes are commonly used to
describe the data an endpoint expects from the client.

For example, when a client creates a post, the API may expect:

  • content: a string.
  • visibility: either "public" or "private".

We can describe that structure with a DTO.

export class CreatePostDto {
  content: string;
  visibility: 'public' | 'private';
}
Enter fullscreen mode Exit fullscreen mode

A controller can then use that DTO as the type of its request body:

@Post()
createPost(@Body() createPostDto: CreatePostDto) {
  return this.postsService.create(createPostDto);
}
Enter fullscreen mode Exit fullscreen mode

Why use DTOs instead of accepting any object?

DTOs give the request a clear structure.

They help you:

  • Understand what data an endpoint expects.
  • Keep controller method signatures readable.
  • Reuse request shapes where appropriate.
  • Attach validation rules to incoming fields.
  • Avoid passing arbitrary request data directly into your business logic.

DTOs are not database entities

A DTO and a database entity serve different purposes.


DTO Entity / database model


Describes data accepted or returned Describes how data is represented
by an API operation. or persisted in the database.

Designed around an API use case. Designed around storage and
relationships.

May contain validation rules. Usually contains
persistence-related structure and
behaviour.


For example, a CreateUserDto might accept a password. Your database
user model should store a password hash, not the raw password. The DTO
and database model should not automatically be treated as the same
thing.

A useful naming convention

Common DTO names include:

  • CreatePostDto
  • UpdatePostDto
  • LoginDto
  • RegisterUserDto
  • GetPostsQueryDto

The name should make the purpose of the incoming data clear.


3. What are class-validator and class-transformer?

NestJS commonly uses two packages alongside DTO classes.

class-validator

class-validator checks whether values satisfy validation rules.

For example:

  • Is this value a string?
  • Is this email address valid?
  • Is this number within an allowed range?
  • Is this field optional?
  • Is this string within a certain length?

Install it with:

npm install class-validator
Enter fullscreen mode Exit fullscreen mode

class-transformer

class-transformer helps convert plain JavaScript objects into class
instances and can transform values.

For example, data parsed from an HTTP request normally starts as a plain
object. The ValidationPipe can use class-transformer to turn that
object into an instance of your DTO class.

Install it with:

npm install class-transformer
Enter fullscreen mode Exit fullscreen mode

How are they different?


Package Main responsibility


class-validator Checks whether values follow the
rules defined by decorators.

class-transformer Converts plain objects into class
instances and transforms values.


An easy way to remember:

  • Transformer: prepares or converts the incoming object.
  • Validator: checks whether the prepared data follows the rules.

They work together, but they do different jobs.


4. Adding validation decorators to a DTO

Validation decorators are placed above DTO properties.

import { IsString, IsNotEmpty, MaxLength, IsIn } from 'class-validator';

export class CreatePostDto {
  @IsString()
  @IsNotEmpty()
  @MaxLength(280)
  content: string;

  @IsIn(['public', 'private'])
  visibility: 'public' | 'private';
}
Enter fullscreen mode Exit fullscreen mode

Let's understand the decorators.

Decorator What it checks


@IsString() The value must be a string.
@IsNotEmpty() The value must not be empty.
@MaxLength(280) The string must not exceed 280 characters.
@IsIn([...]) The value must match one of the listed options.

Now, if a client sends a number as content, or uses an unsupported
visibility value, validation can reject the request.

More commonly used decorators

import {
  IsString,
  IsNumber,
  IsBoolean,
  IsEmail,
  IsOptional,
  IsNotEmpty,
  Min,
  Max,
  MinLength,
  MaxLength,
  IsEnum,
  IsArray,
  ArrayMinSize,
  IsUUID,
} from 'class-validator';
Enter fullscreen mode Exit fullscreen mode

Examples:

export class RegisterUserDto {
  @IsString()
  @IsNotEmpty()
  username: string;

  @IsEmail()
  email: string;

  @IsString()
  @MinLength(8)
  password: string;
}
Enter fullscreen mode Exit fullscreen mode
export class PaginationDto {
  @IsNumber()
  @Min(1)
  page: number;

  @IsNumber()
  @Min(1)
  @Max(100)
  limit: number;
}
Enter fullscreen mode Exit fullscreen mode

The decorator only defines the rule. The rules are actually executed
when a validation mechanism, such as NestJS's ValidationPipe,
processes the DTO.


5. Optional fields with @IsOptional()

Sometimes a field is not required.

For example, a user may update their profile without changing their bio.

import { IsOptional, IsString, MaxLength } from 'class-validator';

export class UpdateProfileDto {
  @IsOptional()
  @IsString()
  @MaxLength(160)
  bio?: string;
}
Enter fullscreen mode Exit fullscreen mode

@IsOptional() tells class-validator to skip the other validators for
that property when its value is null or undefined.

The ? in bio?: string is TypeScript syntax. It says the property may
be absent from the type.

These two pieces are related, but they are not identical:

  • ? describes optionality to TypeScript.
  • @IsOptional() affects runtime validation.

If your API should allow an omitted field but reject null, be careful:
@IsOptional() skips validation for both null and undefined. You
may need a different validation rule for that requirement.


6. Nested DTOs and @ValidateNested()

Sometimes the request contains an object inside another object.

Example request:

{
  "content": "Hello",
  "author": {
    "name": "Rohith",
    "email": "rohith@example.com"
  }
}
Enter fullscreen mode Exit fullscreen mode

Create a separate DTO for the nested object:

import { IsEmail, IsString, ValidateNested } from 'class-validator';
import { Type } from 'class-transformer';

export class AuthorDto {
  @IsString()
  name: string;

  @IsEmail()
  email: string;
}

export class CreatePostDto {
  @IsString()
  content: string;

  @ValidateNested()
  @Type(() => AuthorDto)
  author: AuthorDto;
}
Enter fullscreen mode Exit fullscreen mode

Why are both decorators used?

  • @ValidateNested() tells class-validator to validate the nested object.
  • @Type(() => AuthorDto) tells class-transformer what class should be used for that nested value.

Without the transformation metadata, nested plain objects may not be
converted into the expected class instances, so nested validation may
not work as intended.

For an array of nested objects, use @ValidateNested({ each: true })
and @Type(() => SomeDto).


7. What is a Pipe in NestJS?

A Pipe is a class that can run before a route handler receives a value.

A pipe can do two important jobs:

  1. Transformation: Convert incoming data into another form.
  2. Validation: Check incoming data and reject it if it is invalid.

For example, a route parameter arrives as text:

GET /posts/123
Enter fullscreen mode Exit fullscreen mode

The value of id initially comes from the URL as a string. A pipe can
convert it into a number before the controller method uses it.

Where do Pipes fit in the request flow?

A simplified flow looks like this:

Client sends HTTP request
        |
        v
NestJS receives the request
        |
        v
Guards and interceptors (where configured)
        |
        v
Pipes process route arguments
        |
        v
Controller method runs
        |
        v
Service / business logic
        |
        v
Response goes back to the client
Enter fullscreen mode Exit fullscreen mode

The important point for this topic is that pipes process values before
those values are passed to the controller handler.


8. Built-in Pipes

NestJS provides ready-made pipes for common tasks. You do not need to
create your own pipe for every simple conversion or validation.

Some useful built-in pipes are:


Pipe Purpose


ValidationPipe Validates incoming data using DTO
validation metadata.

ParseIntPipe Converts a value to an integer or
throws an error.

ParseFloatPipe Converts a value to a number with a
decimal part or throws an error.

ParseBoolPipe Parses a boolean value or throws an
error.

ParseUUIDPipe Checks whether a value is a valid
UUID.

ParseArrayPipe Parses and validates array input
according to its options.

DefaultValuePipe Supplies a default value when the
input is null or undefined.

ParseEnumPipe Checks whether a value belongs to a
specified enum.


Example: ParseIntPipe

import { Controller, Get, Param, ParseIntPipe } from '@nestjs/common';

@Controller('posts')
export class PostsController {
  @Get(':id')
  findOne(@Param('id', ParseIntPipe) id: number) {
    return { id };
  }
}
Enter fullscreen mode Exit fullscreen mode

If the client requests /posts/123, the pipe converts the route
parameter into the number 123.

If the client requests /posts/abc, parsing fails and NestJS returns an
error instead of passing an invalid number to the controller.

Example: ParseBoolPipe

@Get('status')
getStatus(@Query('active', ParseBoolPipe) active: boolean) {
  return { active };
}
Enter fullscreen mode Exit fullscreen mode

A valid boolean query value is parsed before the handler runs. Invalid
values are rejected.

Example: DefaultValuePipe

import { DefaultValuePipe, ParseIntPipe } from '@nestjs/common';

@Get()
findAll(
  @Query('limit', new DefaultValuePipe(10), ParseIntPipe)
  limit: number,
) {
  return { limit };
}
Enter fullscreen mode Exit fullscreen mode

If the client does not provide limit, the default value 10 is used.
The next pipe then parses the value as an integer.

When multiple pipes are attached to one parameter, they process the
value in the order they are listed.

Example: ParseUUIDPipe

@Get(':id')
findById(
  @Param('id', new ParseUUIDPipe()) id: string,
) {
  return this.postsService.findById(id);
}
Enter fullscreen mode Exit fullscreen mode

This checks the route parameter before it reaches the controller method.


9. What is ValidationPipe?

ValidationPipe is NestJS's built-in pipe for validating incoming
request data using DTO classes and validation decorators.

For example:

@Post()
create(@Body() createPostDto: CreatePostDto) {
  return this.postsService.create(createPostDto);
}
Enter fullscreen mode Exit fullscreen mode

When a ValidationPipe is active, NestJS can use the metadata on
CreatePostDto to validate the body before calling create().

If validation succeeds, the controller receives the validated DTO value.

If validation fails, NestJS throws a BadRequestException by default
and the controller handler is not called.

Important distinction

Writing decorators inside a DTO does not automatically validate every
request.

You need to apply ValidationPipe---locally, at controller or route
level, or globally---so that NestJS actually runs the validation.


10. Setting up ValidationPipe globally

In most applications, it is useful to apply validation globally so that
DTO validation works consistently across controllers.

Open your main.ts file:

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();
Enter fullscreen mode Exit fullscreen mode

Let's understand the three options.

whitelist: true

This removes properties that do not have any validation decorators in
the DTO.

Suppose your DTO is:

export class CreatePostDto {
  @IsString()
  content: string;
}
Enter fullscreen mode Exit fullscreen mode

The client sends:

{
  "content": "Hello",
  "isAdmin": true
}
Enter fullscreen mode Exit fullscreen mode

With whitelist: true, isAdmin is removed because it is not decorated
in the DTO.

forbidNonWhitelisted: true

Instead of silently removing extra properties, this option makes the
request fail when it contains a property that is not allowed by the DTO.

Using the previous example, sending isAdmin results in a validation
error.

This is useful when you want clients to know that they are sending
unsupported fields.

Note: forbidNonWhitelisted is normally used together with
whitelist.

transform: true

This enables transformation of plain incoming objects into DTO class
instances. It also enables supported primitive conversion for route and
query parameters when their types are declared.

For example:

@Get()
findAll(@Query('page') page: number) {
  return { page };
}
Enter fullscreen mode Exit fullscreen mode

With transformation enabled, NestJS can convert a query value such as
"2" into the number 2 when the parameter type metadata is available.

For more complex query validation, a dedicated DTO is usually clearer.

Quick summary


Option What happens


whitelist: true Removes properties without
validation decorators.

forbidNonWhitelisted: true Rejects requests containing those
extra properties.

transform: true Converts plain input into DTO
instances and supports suitable
primitive conversion.



11. Local vs global ValidationPipe

You can apply a pipe at different levels.

Parameter level

@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number) {
  return { id };
}
Enter fullscreen mode Exit fullscreen mode

Only that parameter uses the pipe.

Method level

@Post()
@UsePipes(new ValidationPipe())
create(@Body() dto: CreatePostDto) {
  return this.postsService.create(dto);
}
Enter fullscreen mode Exit fullscreen mode

The pipe applies to the route handler's parameters.

Controller level

@UsePipes(new ValidationPipe())
@Controller('posts')
export class PostsController {
  // Route handlers
}
Enter fullscreen mode Exit fullscreen mode

The pipe applies to handlers in that controller.

Global level

app.useGlobalPipes(new ValidationPipe());
Enter fullscreen mode Exit fullscreen mode

The pipe applies across the application.

For a consistent API, a global ValidationPipe is a common choice. You
can still use specific built-in pipes on individual parameters where
they are needed.


12. Creating a Custom Pipe

Built-in pipes cover common requirements. But sometimes your application
has a rule that is specific to your project.

For example, imagine a route expects a post ID that must be a positive
integer. You can create a custom pipe for that rule.

A custom pipe implements the PipeTransform interface and defines a
transform() method.

import {
  ArgumentMetadata,
  BadRequestException,
  Injectable,
  PipeTransform,
} from '@nestjs/common';

@Injectable()
export class PositiveIntPipe implements PipeTransform {
  transform(value: unknown, metadata: ArgumentMetadata): number {
    const parsedValue = Number(value);

    if (!Number.isInteger(parsedValue) || parsedValue <= 0) {
      throw new BadRequestException('ID must be a positive integer');
    }

    return parsedValue;
  }
}
Enter fullscreen mode Exit fullscreen mode

Understanding the important parts

  • @Injectable() marks the class as a provider that NestJS can manage.
  • PipeTransform is the interface that custom pipes implement.
  • transform() receives the incoming value and metadata about the argument.
  • The method checks the value.
  • If the value is invalid, it throws an exception.
  • If the value is valid, it returns the transformed value.

Using the custom pipe

@Get(':id')
findOne(
  @Param('id', PositiveIntPipe) id: number,
) {
  return this.postsService.findOne(id);
}
Enter fullscreen mode Exit fullscreen mode

Now the controller receives a positive integer. Invalid values are
rejected before the handler runs.

What is ArgumentMetadata?

The metadata parameter gives information about the value being
processed.

It includes:

  • type: where the value comes from, such as body, query, param, or a custom source.
  • metatype: the expected class or type metadata, when available.
  • data: the name of the route argument, such as id.

You do not need to use metadata in every custom pipe, but it is
available when your logic needs to know what kind of argument is being
processed.

When should you create a custom pipe?

Create one when you have a reusable transformation or validation rule
that is not already handled well by a built-in pipe.

Examples include:

  • Checking that an ID is a positive integer.
  • Converting a custom date format.
  • Validating a project-specific route parameter format.
  • Normalizing a value before the controller receives it.

Do not create a custom pipe just to repeat something a built-in pipe
already does.


13. What happens when validation fails?

Suppose your DTO is:

export class CreatePostDto {
  @IsString()
  @IsNotEmpty()
  content: string;
}
Enter fullscreen mode Exit fullscreen mode

The client sends:

{
  "content": 123
}
Enter fullscreen mode Exit fullscreen mode

The flow is:

  1. The request reaches NestJS.
  2. The ValidationPipe processes the request body.
  3. The plain object is transformed as configured.
  4. class-validator checks the DTO rules.
  5. @IsString() fails because the value is a number.
  6. NestJS throws a BadRequestException.
  7. The controller method is not executed.

The exact response body can depend on your NestJS configuration and
exception handling, but the default validation error uses HTTP status
400 Bad Request.

Why is this useful?

The service layer does not need to repeat basic request checks that
belong at the API boundary. It can focus on business rules, while the
DTO and validation pipe handle the structure and format of incoming
data.

Remember that DTO validation does not replace business-level checks. For
example, checking whether a username is already taken usually requires
looking at stored data and belongs in application logic or a dedicated
validation mechanism.


14. Common mistakes to avoid

1. Forgetting to enable ValidationPipe

You may add decorators to a DTO and still find that invalid requests are
accepted. Check that a ValidationPipe is applied.

2. Forgetting to install the packages

If you use decorators from class-validator and class-transformer,
make sure both packages are installed.

npm install class-validator class-transformer
Enter fullscreen mode Exit fullscreen mode

3. Using TypeScript types as if they validate runtime data

This:

create(@Body() dto: CreatePostDto) {}
Enter fullscreen mode Exit fullscreen mode

does not, by itself, validate the request. TypeScript types are not a
replacement for runtime validation.

4. Forgetting decorators on DTO properties

With whitelist: true, properties without validation decorators are
treated as non-whitelisted. If a property should be accepted, add the
appropriate validation decorator.

5. Confusing @IsOptional() with ?

The question mark is for TypeScript. @IsOptional() controls runtime
validation behaviour.

6. Expecting a DTO to check database rules automatically

A DTO can check formats and values, such as whether an email is valid.
It does not automatically know whether that email already exists in your
database.

7. Forgetting nested transformation

For nested DTO validation, remember to use @ValidateNested() and
@Type(() => NestedDto) where appropriate.


15. How these concepts fit together in a real endpoint

Here is a small example that brings the concepts together.

Step 1: Create the DTO

// create-post.dto.ts
import { IsIn, IsNotEmpty, IsString, MaxLength } from 'class-validator';

export class CreatePostDto {
  @IsString()
  @IsNotEmpty()
  @MaxLength(280)
  content: string;

  @IsIn(['public', 'private'])
  visibility: 'public' | 'private';
}
Enter fullscreen mode Exit fullscreen mode

Step 2: Use the DTO in the controller

// posts.controller.ts
import { Body, Controller, Post } from '@nestjs/common';
import { CreatePostDto } from './dto/create-post.dto';

@Controller('posts')
export class PostsController {
  @Post()
  create(@Body() dto: CreatePostDto) {
    return {
      message: 'Post request received',
      data: dto,
    };
  }
}
Enter fullscreen mode Exit fullscreen mode

Step 3: Enable validation globally

// main.ts
import { NestFactory } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  app.useGlobalPipes(
    new ValidationPipe({
      whitelist: true,
      forbidNonWhitelisted: true,
      transform: true,
    }),
  );

  await app.listen(3000);
}

bootstrap();
Enter fullscreen mode Exit fullscreen mode

Step 4: Test the endpoint

A valid request:

POST /posts
Content-Type: application/json
Enter fullscreen mode Exit fullscreen mode
{
  "content": "Learning NestJS validation",
  "visibility": "public"
}
Enter fullscreen mode Exit fullscreen mode

An invalid request:

{
  "content": 123,
  "visibility": "everyone",
  "isAdmin": true
}
Enter fullscreen mode Exit fullscreen mode

The invalid request should be rejected because:

  • content is not a string.
  • visibility is not one of the accepted values.
  • isAdmin is not part of the DTO and is rejected because forbidNonWhitelisted is enabled.

16. Quick revision


Concept Remember it this way


DTO Describes the data an API operation
expects or returns.

class-validator Checks values against validation
decorators.

class-transformer Converts plain objects into class
instances and transforms values.

Pipe Processes a value before the route
handler receives it.

Built-in Pipe A ready-made pipe for common
parsing or validation tasks.

Custom Pipe A pipe you write for your own
transformation or validation rule.

ValidationPipe NestJS pipe that connects DTO
metadata with runtime validation.

whitelist Removes properties without
validation decorators.

forbidNonWhitelisted Rejects requests containing extra,
non-whitelisted properties.

transform Enables DTO instance transformation
and suitable primitive conversion.


17. Interview-style questions to practise

Try answering these without looking at the notes:

  1. What is a DTO, and why do we use it in NestJS?
  2. What is the difference between a DTO and a database entity?
  3. What is the difference between class-validator and class-transformer?
  4. Does adding decorators to a DTO automatically validate a request?
  5. What is a Pipe in NestJS?
  6. What is the difference between transformation and validation?
  7. Name some built-in Pipes in NestJS.
  8. What does ParseIntPipe do when it receives an invalid value?
  9. What is the purpose of ValidationPipe?
  10. What is the difference between whitelist and forbidNonWhitelisted?
  11. Why do we enable transform: true?
  12. How do you create a custom Pipe?
  13. What is the purpose of the transform() method?
  14. Why are @ValidateNested() and @Type() useful together?
  15. Why is DTO validation not a replacement for business logic validation?

Final takeaway

Think of the whole process like this:

  • DTO: describes what the request should look like.
  • Decorators: define the rules for each property.
  • class-transformer: helps turn incoming plain data into class instances.
  • class-validator: checks whether the data follows the rules.
  • Pipes: transform or validate values before the controller receives them.
  • Global ValidationPipe: applies a consistent validation setup across the application.

Once you understand how these pieces work together, request validation
in NestJS becomes much easier to follow and maintain.

These are learning notes with practical examples. Always check the
NestJS and package documentation for version-specific behaviour and
configuration details.

Top comments (0)