Suggest an editImprove this articleRefine the answer for “What are dtos and how does validation work in NestJS?”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**DTO (Data Transfer Object)** in NestJS is a class that defines the shape and validation rules for incoming request data. `ValidationPipe` checks those rules before the request reaches your controller and returns a 400 error if anything fails. ```typescript app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true, })); ``` **Key point:** decorators from `class-validator` are checked at runtime. TypeScript types alone do nothing when an HTTP request arrives.Shown above the full answer for quick recall.Answer (EN)Image**DTO (Data Transfer Object)** in NestJS is a TypeScript class that defines the expected shape and validation rules for incoming API data, checked automatically by `ValidationPipe` before the request reaches your controller. ## Theory ### TL;DR - DTOs are a checkpoint for your API: the request body must match the declared shape and rules, or it gets rejected with a 400 error - `ValidationPipe` runs before controller code, converts plain JSON into typed class instances, and strips extra fields - Always pair DTOs with `class-validator` decorators and a global `ValidationPipe` - Without `whitelist: true`, extra fields like `role: "admin"` pass straight through to your service - Decision rule: public API endpoint gets a DTO; internal service call uses a plain interface ### Quick example ```typescript // create-user.dto.ts import { IsEmail, IsString, MinLength } from 'class-validator'; export class CreateUserDto { @IsEmail() email: string; @IsString() @MinLength(6) password: string; } // users.controller.ts @Post() create(@Body() dto: CreateUserDto) { return this.usersService.create(dto); // already validated here } // POST /users { "email": "test@example.com", "password": "short" } // → 400 { message: ['password must be longer than 6 characters'] } // POST /users { "email": "test@example.com", "password": "secret123" } // → 201 Created ``` `ValidationPipe` converts the raw JSON body into a `CreateUserDto` instance and checks every decorator. If anything fails, NestJS throws a `BadRequestException` before your code runs. ### How ValidationPipe works `ValidationPipe` sits between the HTTP layer and your controllers. When a request arrives, it goes through four steps. First, `class-transformer` converts the plain JSON object into a real class instance. With `transform: true` set, the string `"25"` becomes the number `25`. Second, `class-validator` reads the decorator metadata through TypeScript reflection and checks each rule. Third, on failure, NestJS throws a `BadRequestException` with a detailed list of messages. Fourth, on success, the typed object lands in your controller parameter. This requires `emitDecoratorMetadata: true` in `tsconfig.json` and the `reflect-metadata` polyfill. NestJS CLI projects have both by default. ### Global setup ```typescript // main.ts app.useGlobalPipes(new ValidationPipe({ whitelist: true, // strip properties not declared in DTO forbidNonWhitelisted: true, // 400 if extra props are sent transform: true, // string "25" → number 25 })); ``` All three options together give you what most teams actually want in production. Without `transform: true`, query params and URL segments stay as strings, so `@IsInt()` will fail on `age=25` from a URL. ### When to use - Public API endpoint → DTO with decorators and `ValidationPipe` - Update endpoint → `PartialType(CreateUserDto)` makes all fields optional automatically - Query params → `@Query() dto: GetUsersDto` works the same as `@Body()` - Internal service call → plain TypeScript interface, no validation needed - File upload → DTO for metadata only; the file itself goes through `FileInterceptor` ### Common mistakes **1. Forgetting `transform: true`** ```typescript // Request: GET /users?age=25 export class GetUsersDto { @IsInt() age: number; // fails - "25" is a string without transform } // Fix: set transform: true in ValidationPipe ``` Query params and path variables arrive as strings. Without `transform: true`, `@IsInt()` rejects them even when the value looks like a number. **2. No `whitelist: true`** ```typescript // Client sends: { email: "user@x.com", role: "admin" } // Without whitelist: true → role: "admin" reaches your service // Fix: whitelist: true strips unknown props automatically // Stronger fix: forbidNonWhitelisted: true returns 400 on extra props ``` This is the most common security gap I've seen in NestJS codebases. One stray property and you've handed someone an unintended escalation path. **3. Nested DTOs without `@Type()`** ```typescript // Wrong - items stays a plain object array, not OrderItemDto instances @ValidateNested({ each: true }) items: OrderItemDto[]; // Correct @ValidateNested({ each: true }) @Type(() => OrderItemDto) items: OrderItemDto[]; ``` `class-transformer` needs `@Type()` to know what class to instantiate. Without it, validation on nested objects does not run at all and bad data passes through. **4. `@IsOptional()` on required fields** ```typescript // Wrong - accepts email: "" (empty string) @IsOptional() @IsEmail() email: string; // If email is required, drop @IsOptional() // If it can be absent but must be valid when present, keep it, // but know that "" passes @IsOptional() + @IsEmail() combined ``` **5. Missing `emitDecoratorMetadata` in tsconfig** If decorators seem to do nothing, check `tsconfig.json`. Both `experimentalDecorators: true` and `emitDecoratorMetadata: true` are required. NestJS CLI adds them, but projects configured manually often miss the second one. ### Real-world usage - NestJS + Prisma: DTO validates before `prisma.user.create()`, so the database never receives invalid data - NestJS + Swagger: add `@ApiProperty()` to DTO fields and OpenAPI docs generate automatically - Microservices: DTOs define message contracts between services in NestJS hybrid apps - Frontend integration: OpenAPI spec generated from DTOs can produce TypeScript types for React forms via tools like `openapi-typescript` ### Follow-up questions **Q:** What happens if you skip `ValidationPipe` entirely? **A:** The raw JSON body arrives in your controller as `any`. TypeScript types are erased at runtime, so the shape annotations on your DTO do nothing. You would need to validate manually at every endpoint. **Q:** How does `class-validator` find the decorator rules at runtime? **A:** Via TypeScript metadata reflection. `reflect-metadata` stores decorator data attached to each class property, and `class-validator` reads it when `validate()` runs. This is why `emitDecoratorMetadata: true` is not optional. **Q:** What is the difference between `@IsDefined()` and `@IsNotEmpty()`? **A:** `@IsDefined()` only rejects `null` and `undefined`. `@IsNotEmpty()` also rejects empty string `""` and empty array `[]`. For most string fields, `@IsNotEmpty()` is what you actually want. **Q:** Can you validate query params the same way? **A:** Yes. `@Query() dto: GetUsersDto` with `ValidationPipe` behaves identically to `@Body()`. Just make sure `transform: true` is set, since query param values are always strings. **Q (senior):** Explain validation groups and a custom `exceptionFactory`. **A:** Validation groups let you apply decorators conditionally. `@IsUnique({ groups: ['create'] })` runs only when you pass `groups: ['create']` to `ValidationPipe`, useful in multi-step wizards. The `exceptionFactory` option replaces the default 400 response with a custom shape, which matters when clients expect a specific error contract. ## Examples ### Basic: user registration ```typescript // dto/create-user.dto.ts import { IsEmail, IsString, MinLength, MaxLength, IsOptional, IsEnum } from 'class-validator'; export enum UserRole { USER = 'user', ADMIN = 'admin' } export class CreateUserDto { @IsString() @MinLength(2) @MaxLength(50) name: string; @IsEmail() email: string; @IsOptional() @IsEnum(UserRole) role?: UserRole = UserRole.USER; } // users.controller.ts @Post() create(@Body() dto: CreateUserDto) { return this.usersService.create(dto); } ``` A valid request returns 201. A request with `email: "not-an-email"` gets `400 { message: ["email must be an email"] }`. The response also tells you exactly which field failed and why. ### Intermediate: e-commerce order with nested items ```typescript // dto/create-order.dto.ts import { IsUUID, IsInt, Min, IsPositive, ValidateNested } from 'class-validator'; import { Type } from 'class-transformer'; export class OrderItemDto { @IsUUID() productId: string; @IsInt() @Min(1) quantity: number; } export class CreateOrderDto { @ValidateNested({ each: true }) @Type(() => OrderItemDto) items: OrderItemDto[]; @IsPositive() total: number; } // orders.controller.ts @Post() create(@Body() dto: CreateOrderDto) { return this.ordersService.placeOrder(dto); } ``` Send `items: [{ productId: "not-a-uuid", quantity: 0 }]` and the response is `400 { message: ["items.0.productId must be a UUID", "items.0.quantity must not be less than 1"] }`. The path in the error message tells you exactly which item in the array failed. ### Advanced: custom validation decorator ```typescript // decorators/is-slug.decorator.ts import { registerDecorator, ValidationOptions } from 'class-validator'; export function IsSlug(options?: ValidationOptions) { return (object: object, propertyName: string) => { registerDecorator({ name: 'isSlug', target: object.constructor, propertyName, options, validator: { validate(value: unknown) { return typeof value === 'string' && /^[a-z0-9-]+$/.test(value); }, defaultMessage: () => `${propertyName} must contain only lowercase letters, numbers, and hyphens`, }, }); }; } // dto/create-post.dto.ts export class CreatePostDto { @IsString() title: string; @IsSlug() slug: string; // "my-post-title" passes, "My Post Title" fails } ``` Custom decorators follow the same registration pattern as built-in ones. For async validation, like checking if an email already exists in the database, return `Promise<boolean>` from `validate()`. `ValidationPipe` handles async validators automatically.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.