DTOs and validation
The boundary where untrusted input becomes a typed object. Get this right and most of your input-handling bugs disappear.
A DTO — data transfer object — is a class describing the shape of a request. Decorate it, register the global ValidationPipe, and Nest rejects anything that does not match before your controller runs.
import { IsEmail, IsInt, IsOptional, IsString, Length, Max, Min } from 'class-validator';
export class CreateUserDto {
@IsEmail()
email!: string;
@IsString()
@Length(2, 60)
name!: string;
@IsOptional()
@IsInt()
@Min(13)
@Max(130)
age?: number;
}app.useGlobalPipes(new ValidationPipe({
whitelist: true, // strip properties with no decorator
forbidNonWhitelisted: true, // ...or reject the request outright
transform: true, // instantiate the DTO class, coerce types
transformOptions: { enableImplicitConversion: true },
}));Why whitelist is the important one#
Without it, a client can send fields you never declared and they arrive in your DTO. If any code path spreads that object into a database update, you have mass assignment:
// without whitelist, this is a privilege escalation
await this.prisma.user.update({ where: { id }, data: { ...dto } });
// client sent { name: "x", role: "admin" }whitelist: true strips undeclared properties. forbidNonWhitelisted: true rejects the request instead, which is better in an internal API where a surprise field means a client bug.
Transformation#
With transform: true, Nest instantiates the DTO class rather than handing you a plain object. That means instanceof works, defaults apply, and class-transformer decorators run.
export class ListUsersDto {
@Type(() => Number) // query params are always strings
@IsInt() @Min(1) @Max(100)
limit = 20;
@Transform(({ value }) => value?.trim().toLowerCase())
@IsOptional() @IsString()
search?: string;
}Nested objects and arrays#
Nested validation does not recurse unless you ask.
export class CreateOrderDto {
@ValidateNested({ each: true })
@Type(() => OrderLineDto) // required, or nested validation silently passes
@ArrayMinSize(1)
lines!: OrderLineDto[];
}Omitting @Type() is the most common validation bug in Nest: the array is accepted, the items are never checked.
Partial updates#
import { PartialType } from '@nestjs/mapped-types';
export class UpdateUserDto extends PartialType(CreateUserDto) {}Every field becomes optional, decorators are preserved. PickType, OmitType and IntersectionType compose similarly.
Response shaping#
Validation guards the way in. class-transformer guards the way out.
export class UserEntity {
id!: string;
email!: string;
@Exclude()
passwordHash!: string;
}@UseInterceptors(ClassSerializerInterceptor)
@Controller('users')
export class UsersController {}Now passwordHash cannot leak, even if a service returns the raw database row. Worth doing on every entity that has a secret in it — it converts a class of accidental disclosure into an impossibility.
An alternative: schema-first#
If you prefer schemas to decorators, nestjs-zod gives you a Zod schema as the single source of truth for both runtime validation and the TypeScript type, which removes the drift between the two.
const CreateUser = z.object({ email: z.email(), name: z.string().min(2) });
export class CreateUserDto extends createZodDto(CreateUser) {}Common questions#
Do I need DTOs if I already have TypeScript types?#
Yes. Types are erased at compile time — they constrain your code, not the incoming JSON. A DTO with validators is the only thing actually checking what arrived over the wire.
Where do business rules go?#
In the service. The DTO checks shape and format ("is this a valid email"); the service checks state ("is this email already registered"), because that needs the database.
Why is my nested array not being validated?#
Almost certainly a missing @Type(() => Child) alongside @ValidateNested({ each: true }). Without it, class-transformer does not know what class to instantiate and validation of the items is skipped silently.
Get the NestJS agent pack
A battle-tested AGENTS.md, the review checklist, and the failure-mode cheat sheet for NestJS. One email, then occasional updates when the tooling shifts. No course pitch.
AGENTS.md now — no email needed.