← Dev log

backend

DTO validation as a real security boundary

DTO validation as a real security boundary

class-transformer turns an incoming JSON body into a real class instance; class-validator runs decorator-declared rules against it. Wired into NestJS's ValidationPipe, a controller declares its expected shape once as a class and gets request validation for free.

export class CreateApplicationDto {
  @IsUUID()
  program_id: string;

  @IsOptional()
  @IsUUID()
  child_id?: string;
}
app.useGlobalPipes(new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true }));

The whitelist options are the point

whitelist: true strips any property not declared on the DTO before it reaches the service layer. forbidNonWhitelisted: true rejects the whole request instead of silently dropping the extras.

This is a real security boundary, not neatness. Without it, a client sending { ..., role: "admin" } on a user-update endpoint gets through if the service carelessly spreads the body into an update call. Mass assignment, closed off at validation rather than trusted to every individual handler to remember.

I learned the flip side of this the hard way: a DTO with import type instead of import, or a property with no decorator at all, isn't recognised under whitelist: true — so the pipe strips the entire payload and the endpoint rejects every field with a confusing "unrecognised property" error. I hit that shape of bug in three different files for three different reasons before I stopped fixing them one at a time and grepped the whole codebase for the pattern.

Beyond field checks: @ValidateNested() + @Type(() => NestedDto) for nested objects, @ValidateIf for conditional rules, and custom validators for domain-specific formats — a government ID pattern, a phone format — not just generic type checks. The DTOs double as documentation, and NestJS can generate Swagger straight from the decorators.