Foundations View as Markdown

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.

src/users/dto/create-user.dto.ts
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;
}
src/main.ts
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:

ts
// 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.

ts
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.

ts
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#

ts
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.

src/users/entities/user.entity.ts
export class UserEntity {
  id!: string;
  email!: string;

  @Exclude()
  passwordHash!: string;
}
ts
@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.

ts
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.

Unsubscribe in one click. We never sell the list. Or just take the AGENTS.md now — no email needed.