DTOs in NestJS
A DTO (Data Transfer Object) defines the shape of data crossing your application’s boundary — typically the body of an incoming HTTP request. In NestJS a DTO is always a class, never an interface, and that choice is not stylistic: validation, transformation, and Swagger generation all need the type to exist at runtime, and only classes survive compilation. This page explains the class-vs-interface rule, shows how to compose DTOs with class-validator decorators, and covers patterns like partial update DTOs and nested objects.
Why a class and not an interface
TypeScript interfaces are erased during compilation; nothing about them exists when your code runs. class-validator and class-transformer work by reading metadata off a real constructor, so an interface gives them nothing to inspect. A class produces a runtime value that can carry decorator metadata.
// ❌ Erased at runtime — ValidationPipe can validate nothing
interface CreateUserDto {
email: string;
age: number;
}
// ✅ Exists at runtime, carries validation metadata
import { IsEmail, IsInt, Min } from "class-validator";
export class CreateUserDto {
@IsEmail()
email!: string;
@IsInt()
@Min(18)
age!: number;
}
The ! definite-assignment assertion tells the compiler these fields are initialized externally (by Nest, from the request), so strictPropertyInitialization won’t complain. The decorators attach validation rules that the ValidationPipe reads at runtime.
Using a DTO in a controller
Annotate the handler parameter with @Body() and the DTO type. When a global ValidationPipe is active, Nest validates the payload against the decorators and rejects bad input with a 400 before your method runs.
import { Body, Controller, Post } from "@nestjs/common";
@Controller("users")
export class UsersController {
@Post()
create(@Body() dto: CreateUserDto) {
// dto is validated and typed here
return this.users.create(dto);
}
}
The DTO doubles as documentation: the type tells future readers exactly what the endpoint accepts, and editor autocomplete works on dto throughout the method.
Optional fields and update DTOs
Mark optional fields with ? plus @IsOptional() so the validator skips them when absent. For update endpoints, derive a partial DTO from your create DTO with the PartialType mapped-type helper instead of redeclaring every field.
import { PartialType } from "@nestjs/mapped-types";
import { IsOptional, IsString } from "class-validator";
export class CreateUserDto {
@IsEmail()
email!: string;
@IsString()
@IsOptional()
bio?: string;
}
// Every field becomes optional, decorators preserved
export class UpdateUserDto extends PartialType(CreateUserDto) {}
PartialType rebuilds the class with all properties optional and re-applies the validation metadata, so UpdateUserDto stays in sync with CreateUserDto automatically. Related helpers are PickType, OmitType, and IntersectionType.
| Helper | Effect | Mirrors TS utility |
|---|---|---|
PartialType(T) | all props optional | Partial<T> |
PickType(T, [k]) | keep only listed props | Pick<T, K> |
OmitType(T, [k]) | drop listed props | Omit<T, K> |
IntersectionType(A, B) | merge both | A & B |
Nested objects and arrays
For nested DTOs, the validator needs @ValidateNested() plus @Type() from class-transformer so it knows which class to instantiate for the nested value.
import { Type } from "class-transformer";
import { IsString, ValidateNested } from "class-validator";
export class AddressDto {
@IsString()
city!: string;
}
export class CreateOrderDto {
@ValidateNested()
@Type(() => AddressDto)
address!: AddressDto;
@ValidateNested({ each: true })
@Type(() => AddressDto)
shippingHistory!: AddressDto[];
}
Without @Type(), class-transformer leaves the nested value as a plain object and class-validator cannot validate it. The { each: true } option applies the rule to every element of an array.
Do not put business logic or methods on DTOs. They are plain data shapes; logic belongs in services. A DTO that grows methods is usually trying to become an entity.
Best Practices
- Always declare DTOs as classes — interfaces cannot be validated or transformed at runtime.
- Co-locate validation decorators on the DTO so the contract and its rules live together.
- Use the definite-assignment
!for required fields populated by the framework. - Derive update DTOs with
PartialTypeinstead of duplicating create DTOs. - Add
@ValidateNested()+@Type()for nested objects and arrays. - Keep DTOs free of methods and business logic; they are pure data shapes.
- Enable a global
ValidationPipeso DTO rules are actually enforced.