DTOs & Validation
How request and response DTOs are decorated to validate input and document the API, and what the global pipe does and does not do.
A DTO is a decorated class describing the shape of a request or response body. Not an entity, never touches the database. Every endpoint with a body takes one in and is typed by one out.
Two decorators per field
Every field carries both — a class-validator rule (enforcement, 400 on bad input) and an @nestjs/swagger decorator (OpenAPI schema, feeds the generated client).
@IsString()
@IsNotEmpty()
@ApiProperty()
name: string;
@IsString()
@IsOptional()
@ApiProperty({ required: false })
description?: string;- Required field → validator +
@ApiProperty(). - Optional field →
@IsOptional()+@ApiProperty({ required: false })(or@ApiPropertyOptional).
No @ApiProperty means no field
A validated field with no @ApiProperty never enters the schema — the generated client can't send it even though the backend accepts it.
The global pipe is bare
main.ts enables validation with a bare ValidationPipe — no options object:
app.useGlobalPipes(new ValidationPipe());It does exactly one thing: runs each field against its class-validator decorators, throws 400 on failure. Three behaviors are off because no options were passed:
| Option | State | Consequence |
|---|---|---|
whitelist | off | Unknown body keys are not stripped — they ride along. |
forbidNonWhitelisted | off | Unknown keys do not error. |
transform | off | No global type coercion — values arrive as-is. |
No whitelist, no transform
Design DTOs knowing the pipe only validates. Coercion is opt-in per field (below).
Coercing query params
No global transform → URL values stay strings (?limit=10 → "10"). Opt a field in with class-transformer's @Type, ordered before the numeric/date checks:
@IsOptional()
@Type(() => Number)
@IsInt()
@Min(1)
@Max(100)
@ApiPropertyOptional({ default: 10, description: 'Max results per page (1-100)' })
limit?: number;@Type(() => Number)runs first, casts"10"→10, then@IsInt/@Min/@Maxcheck the number.- Drop
@Typeand@IsIntrejects the string — every paginated request 400s. - Dates:
@Type(() => Date)before@IsDate.
@Type is per-field
A misbehaving numeric or date query param is almost always missing its @Type(() => …).
Create, Update, Response
Most resources carry three DTO shapes, each built differently:
| Shape | How | File |
|---|---|---|
| Create | Hand-written — validators + @ApiProperty on every field | create-<resource>.dto.ts |
| Update | extends PartialType(CreateDto) — re-emits all fields optional | update-<resource>.dto.ts |
| Response | extends DatabaseEntityDto implements <Interface> | <resource>.dto.ts |
export class UpdateCampusDto extends PartialType(CreateCampusDto) {}
export class CampusDto extends DatabaseEntityDto implements Campus {
@IsString()
@IsNotEmpty()
@ApiProperty()
name: string;
}DatabaseEntityDtosupplies a documentedid+ timestamps — don't redeclare them.implements Campuskeeps the class in lockstep with the shared interface — see Entities, Tables & Types.
PartialType from @nestjs/swagger
Import PartialType from @nestjs/swagger, not @nestjs/mapped-types — only the swagger one preserves @ApiProperty metadata. OmitType / IntersectionType (also swagger) handle drop-a-field and merge.
Validating nested objects
@IsObject() only checks "has keys" — not what's inside. Two patterns:
-
Light — document the type, trust the shape loosely. Use for trusted nested data.
apps/server/src/app/campus/dto/create-campus.dto.ts @IsObject() @IsNotEmpty() @ApiProperty({ type: CreateAddressDto, required: true }) address: CreateAddressDto; -
Strict — recurse into the nested DTO's validators. Use for untrusted data or arrays.
apps/server/src/app/role/dto/create-role.dto.ts @IsArray() @ValidateNested({ each: true }) @Type(() => PermissionDto) @ApiProperty({ type: [PermissionDto] }) permissions: PermissionDto[];
@ValidateNested({ each: true }) runs each element through PermissionDto; @Type(() => PermissionDto) tells class-transformer which class to instantiate. The two always travel together.
What gets documented
- Add
descriptionto@ApiPropertyonly when the field name isn't self-explanatory. - Use
@ApiPropertyOptional(orrequired: false) for anything@IsOptional, so client and rules agree. - Keep controller concerns (
operationId, per-status responses) off the DTO — see API Conventions.
Where to go next
API Conventions
Controller-level operationId, responses, and the rest of the contract.
Querying & Pagination
Where the coerced CursorPaginationQueryDto actually gets used.
Entities, Tables & Types
The shared interfaces that response DTOs implement.
Add an Endpoint
Wire a new DTO into a controller method end to end.