Skip to content
NestJS best practices 4 min read

Validation & DTO Best Practices

Validation is the boundary that protects your application from malformed, malicious, or unexpected input. In NestJS the recommended approach is declarative: define Data Transfer Objects (DTOs) with class-validator decorators and let a global ValidationPipe enforce them automatically. Done consistently, this eliminates scattered manual checks, keeps controllers thin, and gives you a single, predictable place to reason about what data is allowed into the system.

Enable the global ValidationPipe

Register the ValidationPipe once at the application root so every incoming payload is validated and transformed without per-handler boilerplate. The whitelist option strips properties that have no decorator, forbidNonWhitelisted rejects them outright, and transform converts plain JSON into real DTO class instances (and coerces primitives like route params).

// src/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,
      transformOptions: { enableImplicitConversion: true },
    }),
  );

  await app.listen(3000);
}
bootstrap();

Install the runtime dependencies the pipe relies on:

npm install class-validator class-transformer

The ValidationPipe is a no-op unless class-validator and class-transformer are installed. Without them NestJS silently skips validation, which is a common source of “my decorators do nothing” confusion.

Design focused input DTOs

A DTO describes exactly what a single endpoint accepts. Keep it small, decorate every field, and never reuse a database entity as a request body. Entities carry persistence concerns (ids, timestamps, relations, internal flags) that should never be settable by clients.

// src/users/dto/create-user.dto.ts
import { IsEmail, IsString, MinLength, IsInt, Min, Max, IsOptional } from 'class-validator';

export class CreateUserDto {
  @IsString()
  @MinLength(2)
  name: string;

  @IsEmail()
  email: string;

  @IsString()
  @MinLength(8)
  password: string;

  @IsOptional()
  @IsInt()
  @Min(13)
  @Max(120)
  age?: number;
}

Because whitelist is on, a request containing an isAdmin: true field it shouldn’t have will simply have that property removed — and with forbidNonWhitelisted, rejected with a 400. This closes mass-assignment vulnerabilities by default.

Reduce duplication with mapped types

Update and partial DTOs usually mirror the create DTO. Instead of copy-pasting fields, derive them with the helpers from @nestjs/mapped-types (or @nestjs/swagger, which also preserves OpenAPI metadata). This keeps validation rules in one place.

// src/users/dto/update-user.dto.ts
import { PartialType, OmitType, PickType } from '@nestjs/mapped-types';
import { CreateUserDto } from './create-user.dto';

// All fields optional, same rules as CreateUserDto
export class UpdateUserDto extends PartialType(CreateUserDto) {}

// Reuse rules but exclude sensitive fields
export class UpdateProfileDto extends OmitType(CreateUserDto, ['password'] as const) {}

// Only the fields you want
export class LoginDto extends PickType(CreateUserDto, ['email', 'password'] as const) {}
HelperEffectTypical use
PartialTypeMakes every field optional, keeps decoratorsPATCH / update endpoints
PickTypeSelects a subset of fieldsLogin, search, narrow forms
OmitTypeRemoves specific fieldsDrop secrets or server-set fields
IntersectionTypeMerges two DTOs into oneCompose shared field groups

Separate input from entities and responses

Map validated DTOs onto your domain objects inside the service layer — never persist a raw request body. For responses, expose a dedicated shape and hide internals with class-transformer’s @Exclude/@Expose, combined with the ClassSerializerInterceptor.

// src/users/entities/user.entity.ts
import { Exclude } from 'class-transformer';

export class User {
  id: string;
  name: string;
  email: string;

  @Exclude()
  password: string;

  constructor(partial: Partial<User>) {
    Object.assign(this, partial);
  }
}
// src/users/users.controller.ts
import { Body, Controller, Post, UseInterceptors, ClassSerializerInterceptor } from '@nestjs/common';
import { CreateUserDto } from './dto/create-user.dto';
import { UsersService } from './users.service';
import { User } from './entities/user.entity';

@Controller('users')
@UseInterceptors(ClassSerializerInterceptor)
export class UsersController {
  constructor(private readonly usersService: UsersService) {}

  @Post()
  async create(@Body() dto: CreateUserDto): Promise<User> {
    return this.usersService.create(dto);
  }
}

A request missing required fields now produces a clean, consistent error:

Output:

{
  "statusCode": 400,
  "message": [
    "email must be an email",
    "password must be longer than or equal to 8 characters"
  ],
  "error": "Bad Request"
}

Avoid @IsOptional() on fields that must merely be nullable. @IsOptional() skips all other validators when the value is undefined. To allow explicit null, prefer @ValidateIf((o) => o.value !== null) so the remaining rules still run.

Validate nested objects and arrays

Nested DTOs are not validated automatically — you must mark them with @ValidateNested() and tell class-transformer how to instantiate them via @Type().

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

class AddressDto {
  @IsString()
  city: string;
}

export class CreateOrderDto {
  @ValidateNested({ each: true })
  @ArrayMinSize(1)
  @Type(() => AddressDto)
  addresses: AddressDto[];
}

Best Practices

  • Register a single global ValidationPipe with whitelist, forbidNonWhitelisted, and transform enabled rather than decorating handlers individually.
  • One DTO per request shape; never accept or persist database entities directly to prevent mass-assignment.
  • Derive update/partial DTOs with PartialType, PickType, and OmitType so validation rules stay DRY.
  • Keep transformation and persistence mapping in the service layer; controllers should only receive validated DTOs.
  • Use @Exclude() plus ClassSerializerInterceptor to strip secrets from responses instead of hand-building output objects.
  • Always pair @ValidateNested() with @Type() for nested objects and arrays, or they pass through unchecked.
  • Quote and version your class-validator/class-transformer dependencies together — mismatched versions cause silent validation failures.
Last updated June 14, 2026
Was this helpful?