Chapter 34 — DTOs and Validation
📖 Definition
A DTO (Data Transfer Object) is a class that defines the shape of incoming or outgoing data. In NestJS, DTOs are validated using class-validator decorators applied through the ValidationPipe.
🔍 Why DTOs?
- Validation in one place — decorators close to the data.
- Type safety — controllers receive a typed payload.
- Documentation — Swagger reads the same decorators.
- Security —
whiteliststrips unknown fields (mass assignment protection).
💻 Code Example — DTO with Validation
import { IsEmail, IsString, MinLength, IsOptional, IsEnum } from "class-validator";
export enum Role { ADMIN = "admin", USER = "user" }
export class CreateUserDto {
@IsEmail()
email: string;
@IsString()
@MinLength(8)
password: string;
@IsOptional()
@IsEnum(Role)
role?: Role;
}💻 Code Example — Global ValidationPipe
// main.ts
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe({
whitelist: true, // strip unknown fields
forbidNonWhitelisted: true, // reject if extra fields exist
transform: true, // transform plain JSON → DTO class
transformOptions: { enableImplicitConversion: true },
}));
await app.listen(3000);
}💻 Code Example — Using the DTO
@Controller("users")
export class UserController {
@Post()
create(@Body() dto: CreateUserDto) {
// dto is already validated. If invalid, ValidationPipe responds 400.
return this.userService.create(dto);
}
}Bad request body:
{ "email": "not-an-email", "password": "123" }Nest auto-responds:
{
"statusCode": 400,
"message": [
"email must be an email",
"password must be longer than or equal to 8 characters"
],
"error": "Bad Request"
}💻 Code Example — Common Decorators
@IsString()
@IsNumber()
@IsBoolean()
@IsDate()
@IsArray()
@IsEmail()
@IsUrl()
@IsUUID()
@IsEnum(MyEnum)
@MinLength(3)
@MaxLength(50)
@Min(0)
@Max(120)
@Length(3, 50)
@Matches(/^[A-Z]/)
@IsOptional() // skip validation if undefined
@IsNotEmpty()
@IsDefined() // must not be undefined
@ArrayMinSize(1)
@ArrayMaxSize(10)💻 Code Example — Nested DTOs
class AddressDto {
@IsString() street: string;
@IsString() city: string;
@IsString() country: string;
}
class CreateUserDto {
@IsString() name: string;
@ValidateNested()
@Type(() => AddressDto) // required by class-transformer
address: AddressDto;
@ValidateNested({ each: true })
@Type(() => AddressDto)
pastAddresses: AddressDto[];
}💻 Code Example — Custom Validator
import { ValidatorConstraint, ValidatorConstraintInterface, registerDecorator } from "class-validator";
@ValidatorConstraint({ async: true })
export class IsUniqueEmail implements ValidatorConstraintInterface {
constructor(private readonly userService: UserService) {}
async validate(email: string) {
return !(await this.userService.findByEmail(email));
}
defaultMessage() { return "Email already in use"; }
}
// Usage
class CreateUserDto {
@IsEmail()
@Validate(IsUniqueEmail)
email: string;
}💻 Code Example — Transform (e.g., Trim, Lowercase)
import { Transform } from "class-transformer";
class CreateUserDto {
@Transform(({ value }) => value?.toLowerCase().trim())
@IsEmail()
email: string;
}💻 Code Example — Partial Update (PATCH) DTO
import { PartialType } from "@nestjs/mapped-types";
export class UpdateUserDto extends PartialType(CreateUserDto) {}
// All CreateUserDto fields become optional automatically.💻 Code Example — Query Param DTO
class ListUsersQueryDto {
@IsOptional() @IsInt() @Type(() => Number) @Min(1) page: number = 1;
@IsOptional() @IsInt() @Type(() => Number) @Min(1) @Max(100) limit: number = 20;
@IsOptional() @IsString() search?: string;
}
@Get()
list(@Query() query: ListUsersQueryDto) {
return this.userService.list(query);
}💻 Code Example — Plain JS Validation (Joi / Zod for Express)
// Zod equivalent
import { z } from "zod";
const CreateUserSchema = z.object({
email: z.string().email(),
password: z.string().min(8),
role: z.enum(["admin", "user"]).optional(),
});
app.post("/users", (req, res) => {
const result = CreateUserSchema.safeParse(req.body);
if (!result.success) return res.status(400).json(result.error.flatten());
// result.data is now strongly typed
});🌍 Real-World Impact
- DTOs catch ~80% of bad requests at the controller boundary.
- Swagger picks up the same decorators → docs stay in sync.
whitelist: trueprevents over-posting attacks (extra fields likeisAdmin).
🎯 Likely Interview Questions
- What is a DTO?
- How does NestJS validate requests?
- What does
whitelist: truedo in ValidationPipe? - How do you validate nested objects? —
@ValidateNested() + @Type(() => Nested). - How would you do this in Express? — Joi, Zod, express-validator.