Chapter 06 — DTOs & Validation
Chapter 06 — DTOs & Validation
Hey everyone! Welcome back to Namaste NestJS! 🙏
Welcome to Season 2 — The Request Pipeline. Over the next few chapters we follow a request on its full journey and meet every checkpoint it passes. Today's checkpoint answers a scary question: "What if the client sends garbage?" — a missing field, a string where a number should be, an email that isn't an email. The answer is DTOs + validation, and it's beautifully automatic in Nest.
What we will cover:
- What a DTO is and why inline types aren't enough
- Creating a DTO class
- class-validator decorators (@IsString, @IsEmail...)
- Turning on the global ValidationPipe
- What a rejected request looks like
- whitelist, transform, and other useful options
- Nested objects and arrays
- Common mistakes
- Interview Questions
1. What is a DTO?
DTO stands for Data Transfer Object. It's a class that describes the shape of data coming into (or going out of) your app.
┌─────────────────────────────────────────────────────────────┐ │ WHAT IS A DTO? │ ├─────────────────────────────────────────────────────────────┤ │ │ │ DTO = a class that defines "what a valid request body │ │ should look like." │ │ │ │ It is the CONTRACT between the client and your API. │ │ │ │ client sends JSON ──→ [ DTO checks it ] ──→ your code │ │ │ │ │ bad? → rejected 400 │ │ │ └─────────────────────────────────────────────────────────────┘
Q: I already used an inline type { name: string } in Chapter 03. Why do I need a DTO class?
A: Because TypeScript types vanish at runtime. They help your editor, but they do nothing when a real request arrives — a hacker can still send { age: "banana" }. A DTO class survives to runtime and can carry validation rules that actually run.
2. Creating a DTO Class
By convention, DTOs live in a dto/ folder and are named *.dto.ts.
// src/cats/dto/create-cat.dto.ts
export class CreateCatDto {
name: string;
age: number;
breed: string;
}
Use it in the controller by typing the @Body:
import { CreateCatDto } from './dto/create-cat.dto';
@Post()
create(@Body() createCatDto: CreateCatDto) { // 👈 typed with the DTO
return this.catsService.create(createCatDto);
}
Right now this is just documentation — nothing is enforced yet. Let's add the teeth.
3. Adding Validation Rules — class-validator
Install the two helper libraries (Nest is built to use them):
npm install class-validator class-transformer
Now decorate each DTO field with rules from class-validator:
// src/cats/dto/create-cat.dto.ts
import { IsString, IsInt, Min, Max, IsOptional, IsEmail } from 'class-validator';
export class CreateCatDto {
@IsString() // must be a string
name: string;
@IsInt() // must be an integer
@Min(0) // ...at least 0
@Max(30) // ...at most 30
age: number;
@IsString()
@IsOptional() // 👈 this field may be missing
breed?: string;
}
Here are the decorators you'll reach for constantly:
| Decorator | Checks that the value is... |
|---|---|
@IsString() | a string |
@IsInt() / @IsNumber() | an integer / any number |
@IsBoolean() | true or false |
@IsEmail() | a valid email address |
@Min(n) / @Max(n) | within a numeric range |
@MinLength(n) / @MaxLength(n) | within a string-length range |
@IsOptional() | allowed to be absent |
@IsEnum(E) | one of an enum's values |
4. Turning It On — The Global ValidationPipe
Decorators alone do nothing until you switch on the ValidationPipe. The best place is globally, in main.ts:
// src/main.ts
import { ValidationPipe } from '@nestjs/common';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe()); // 👈 validate EVERY DTO, everywhere
await app.listen(3000);
}
bootstrap();
That single line makes every DTO in your entire app self-enforcing. Now trace a bad request by hand:
HAND TRACE: POST /cats with body { "name": "Tom", "age": "old" }
─────────────────────────────────────────────────────────────────
1. Request arrives, body = { name: "Tom", age: "old" }
2. ValidationPipe takes the body + the CreateCatDto rules
3. Checks name → "Tom" is a string ✅
4. Checks age → "old" is NOT an integer ❌
5. Validation FAILS → the controller method never runs
▼
Response: 400 Bad Request
{ "message": ["age must be an integer number"], ... } ✔
That's the magic: bad data is rejected before your business logic ever sees it. Your service can trust its inputs completely.
5. Making It Stricter — Useful Options
The ValidationPipe accepts options that make it far more powerful. The three you'll always want:
app.useGlobalPipes(
new ValidationPipe({
whitelist: true, // strip properties NOT in the DTO
forbidNonWhitelisted: true, // ...or throw if extra props are sent
transform: true, // auto-convert payloads to DTO instances
}),
);
| Option | What it does | Why you want it |
|---|---|---|
whitelist | Removes any field not declared in the DTO | Blocks junk / injection of unexpected fields |
forbidNonWhitelisted | Rejects the request if extra fields exist | Fail loudly instead of silently dropping |
transform | Turns the plain body into a real DTO instance and coerces types | e.g. "42" → 42 for a @IsInt field |
Pro tip: transform: true also fixes the "params are strings" pain from Chapter 03 — with it, a route param typed as number arrives already converted.
6. Nested Objects & Arrays
Real payloads have nested data. To validate a nested object, you need @ValidateNested plus @Type (from class-transformer) so the validator knows the inner class.
import { Type } from 'class-transformer';
import { ValidateNested, IsString } from 'class-validator';
class AddressDto {
@IsString() city: string;
@IsString() zip: string;
}
export class CreateOwnerDto {
@IsString()
name: string;
@ValidateNested() // 👈 validate the inner object too
@Type(() => AddressDto) // 👈 tell the transformer its class
address: AddressDto;
}
Note: for an array of nested objects, add { each: true }: @ValidateNested({ each: true }). Without @Type, nested validation silently does nothing — a classic gotcha.
7. Little Traps Beginners Fall Into
┌─────────────────────────────────────────────────────────────┐ │ COMMON MISTAKES ❌ │ ├─────────────────────────────────────────────────────────────┤ │ │ │ Using a TYPE or INTERFACE instead of a CLASS for the DTO. │ │ → Interfaces vanish at runtime. Validation needs a │ │ class. Always `export class XDto`. │ │ │ │ Forgetting app.useGlobalPipes(new ValidationPipe()). │ │ → Your decorators are ignored; garbage gets through. │ │ │ │ Not installing class-validator / class-transformer. │ │ │ │ Nested object won't validate → you forgot @Type(() => X). │ │ │ └─────────────────────────────────────────────────────────────┘
Interview Questions — Quick Fire!
Q: What is a DTO in NestJS?
"A DTO, or Data Transfer Object, is a class that defines the shape of data moving in or out of the application — most often the request body. It acts as a contract between the client and the API, and combined with class-validator it enables automatic validation of incoming payloads."
Q: Why must a DTO be a class and not an interface?
"Because TypeScript interfaces and types are erased at compile time — they don't exist at runtime. Validation and transformation happen at runtime, so they need a real class that persists, along with its decorators, when the app is running."
Q: How does validation actually get triggered?
"You attach class-validator decorators to the DTO fields and enable the ValidationPipe, usually globally in main.ts with app.useGlobalPipes. When a request arrives, the pipe validates the incoming body against the DTO's rules; if it fails, Nest automatically returns a 400 with the validation errors and the handler never runs."
Q: What do whitelist and transform do on the ValidationPipe?
"whitelist strips out any properties that aren't declared in the DTO, which protects against unexpected or malicious fields. transform converts the plain incoming object into an actual instance of the DTO class and coerces primitive types, for example turning a numeric string into a number."
Q: How do you validate a nested object inside a DTO?
"You mark the property with @ValidateNested and add @Type from class-transformer pointing to the nested class, so the validator knows which class to instantiate and check. For arrays of nested objects you also pass { each: true } to @ValidateNested."
Key Points to Remember
| Concept | Key Takeaway |
|---|---|
| DTO | A class describing valid request data — the API contract. |
| Must be a class | Interfaces vanish at runtime; classes survive to validate. |
| class-validator | @IsString, @IsEmail, @Min... declare the rules. |
| ValidationPipe | Enable globally in main.ts to enforce every DTO. |
| Key options | whitelist, forbidNonWhitelisted, transform. |
| Nested data | @ValidateNested + @Type(() => Inner). |
What's Next?
The ValidationPipe is one example of a bigger concept: a Pipe — a checkpoint that transforms or validates data before it reaches your handler. In Chapter 07 we cover Pipes in full: the built-in ones like ParseIntPipe, how to build your own, and exactly where they sit in the request pipeline.
Keep coding, keep shipping! See you in the next one!
Post a Comment