Fullstack CourseLearn by building
Back to week 3

Topic

DTOs & validation

Definition

A Data Transfer Object (DTO) is a class whose class-validator decorators declare the accepted shape and constraints for data crossing an API boundary, enforced at runtime by Nest’s global ValidationPipe.

In simpler words

A DTO is the input checklist for an endpoint — decorators say what’s allowed, and the global pipe rejects anything that breaks the rules before your service runs.

Use request DTOs (e.g. a CreateTicketDto/ListTicketsQueryDto) with class-validator and a global ValidationPipe.

After this you can

  • Read a DTO’s decorators and predict a 400
  • Explain whitelist vs forbidNonWhitelisted
  • Add @Type when a query value needs to become a number

Declaring and enforcing the shape

Definition

The global ValidationPipe — configured with whitelist, forbidNonWhitelisted, and transform — validates every DTO-typed parameter, strips properties without validator decorators, rejects requests containing unknown properties, and converts primitive values to their declared types.

In simpler words

whitelist quietly drops extra fields; forbidNonWhitelisted turns those extra fields into a 400 instead of silently ignoring them; transform makes the DTO instance actually usable as typed data.

CreateTicketDto requires a non-empty, length-bound title and allows an optional description and status.

Every field a client may send needs a decorator — an undecorated field is invisible to whitelist and gets stripped even if the client sends it.

CreateTicketDto

export class CreateTicketDto {
  @IsString()
  @MinLength(1)
  @MaxLength(200)
  title!: string;

  @IsOptional()
  @IsString()
  @MaxLength(5000)
  description?: string;
}

A missing title, an empty string, or a 6000-character description all fail validation before TicketsService.create runs.

Query strings need transform, not just validation

Definition

Query parameters arrive as strings, so a numeric or boolean DTO field needs an explicit transform such as @Type(() => Number) alongside its validator so the validated value has the correct runtime type.

In simpler words

IsInt alone would reject "20" as a string; @Type(() => Number) converts it to a number first, and then IsInt checks the number.

ListTicketsQueryDto applies this to page and limit, with @Min/@Max bounds that double as basic abuse protection on limit.

status and q stay plain strings, validated with @IsEnum and @IsString respectively.

ListTicketsQueryDto (numbers)

@ApiPropertyOptional({ default: 20 })
@IsOptional()
@Type(() => Number)
@IsInt()
@Min(1)
@Max(100)
limit = 20;

Without @Type, "100" would still be a string when @IsInt runs, and validation would fail.

Keep in mind

  • Give every accepted field a validator decorator — whitelist strips the rest.
  • Transform query strings to their real type before validating them.
  • Prefer 400 with field-level messages over letting bad data reach a service.

Test

Check your understanding

At least 10 questions — mix of concept, syntax, practical, and logic. Score ≥80% (enforced by the API) to save progress.

Checking your session…

15 questions · concept 5 · syntax 3 · practical 4 · logic 3

1. What is a request DTO here?
Concept
2. Which ValidationPipe flags are used in this academy style?
Syntax
3. Do TypeScript interfaces validate HTTP bodies at runtime?
Practical
4. What happens to unknown body fields with forbidNonWhitelisted?
Logic
5. Why transform query DTOs?
Concept
6. Where are CreateTicketDto constraints declared?
Practical
7. Does @ApiProperty validate input?
Syntax
8. Should controllers re-validate fields manually?
Logic
9. What is whitelist good for?
Concept
10. ListTicketsQueryDto typically validates what?
Practical
11. Why does invalid input still reach the handler here?
Conceptintermediate
interface CreateTicketDto { title: string; }
@Post()
create(@Body() dto: CreateTicketDto) {}
12. Which request body passes validation for this DTO?
Syntaxintermediate
export class CreateTicketDto {
  @IsString()
  @IsNotEmpty()
  title: string;

  @IsOptional()
  @IsIn(['low', 'high'])
  priority?: string;
}
13. What is the security risk in this update handler?
Practicaladvanced
@Patch(':id')
update(@Body() dto: any, @Param('id') id) {
  return this.repo.update(id, dto);
}
14. What does @Type(() => Number) enable in this query DTO?
Logicintermediate
export class ListQueryDto {
  @Type(() => Number)
  @IsInt()
  @Min(1)
  page = 1;
}
15. A client sends passwordHash in the body. What protects the entity?
Conceptadvanced
// body includes passwordHash: 'x'
new ValidationPipe({ whitelist: true });

Checking your session…