# Validation and DTOs

## 1. Never Trust the Request

Everything in a request — body, query string, path params, headers — is **attacker-controlled text**. TypeScript types do not exist at run time, so this is a lie:

```ts
app.post("/users", (req, res) => {
  const body = req.body as CreateUserInput; // compiles... and checks nothing
  body.age.toFixed(0);                       // 💥 if age is "abc", missing, or an object
});
```

**Validation** turns untrusted `unknown` input into a value you can rely on — or rejects it with a `400` before any business logic runs.

## 2. DTOs: the Shape of the Boundary

A **DTO (Data Transfer Object)** describes exactly what crosses the API boundary for one operation. It is deliberately *not* your database entity:

```ts
// entity: what the database stores
type User = { id: string; email: string; passwordHash: string; role: "user" | "admin"; createdAt: Date };

// input DTO: what a client may SEND to create a user
type CreateUserDto = { email: string; password: string; name?: string };

// output DTO: what the API RETURNS
type UserResponseDto = { id: string; email: string; name?: string };
```

| Why separate DTOs | What goes wrong without them |
|---|---|
| input DTO lists only writable fields | **mass assignment**: client sends `"role": "admin"` and it's saved |
| output DTO lists only public fields | `passwordHash` leaks in a JSON response |
| one DTO per operation | create vs update vs patch have different required fields |
| decouples API from schema | renaming a DB column doesn't break clients |

## 3. Schema Validation with Zod

Schema libraries describe the DTO **once** and derive both the run-time check and the static type:

```ts
import { z } from "zod";

const CreateUserSchema = z.object({
  email: z.string().email().toLowerCase(),
  password: z.string().min(12),
  name: z.string().trim().max(80).optional(),
  age: z.coerce.number().int().min(13).optional(), // "42" → 42 (query strings are always strings)
}).strict();                                        // unknown keys → error (blocks mass assignment)

type CreateUserDto = z.infer<typeof CreateUserSchema>;

app.post("/users", (req, res) => {
  const parsed = CreateUserSchema.safeParse(req.body);
  if (!parsed.success) {
    return res.status(400).json({ errors: parsed.error.flatten().fieldErrors });
  }
  const dto: CreateUserDto = parsed.data; // validated AND typed
  // ...
});
```

## 4. NestJS: class-validator + ValidationPipe

```ts
import { IsEmail, IsOptional, IsString, MinLength, MaxLength } from "class-validator";

export class CreateUserDto {
  @IsEmail() email!: string;
  @IsString() @MinLength(12) password!: string;
  @IsOptional() @IsString() @MaxLength(80) name?: string;
}

// main.ts
app.useGlobalPipes(new ValidationPipe({
  whitelist: true,            // strip properties with no decorator
  forbidNonWhitelisted: true, // ...or reject them with 400
  transform: true,            // turn the plain object into a CreateUserDto instance, coerce types
}));

@Post()
create(@Body() dto: CreateUserDto) { /* dto is already validated */ }
```

The `ValidationPipe` runs in the **pipes** stage of the lifecycle (`07-request_response_lifecycle/`) — after guards, right before the handler.

## 5. Parse, Don't Validate

There are two styles:

- **validate**: `isValid(input): boolean` and then keep using the untyped `input`
- **parse**: `parse(input: unknown): Dto` — returns a *new, typed, normalized* value or throws

Parsing is safer: downstream code can only get a `Dto` by going through the parser, the value is normalized once (trimmed, lowercased, coerced), and the type system tracks that it's been checked.

## 6. Good Validation Errors

```json
HTTP/1.1 400 Bad Request
{
  "error": "validation_failed",
  "details": [
    { "field": "email", "message": "must be a valid email" },
    { "field": "password", "message": "must be at least 12 characters" }
  ]
}
```

- Report **all** field errors at once, not just the first.
- Use stable field paths (`items[2].quantity`) so clients can highlight inputs.
- Don't echo raw input back into HTML (XSS) or include internal details. `backend/api-design/09-error_response_design` covers the full error format.

## 7. Validation Layers

| Layer | Checks | Example |
|---|---|---|
| **Syntactic** (DTO) | shape, types, formats, lengths | `email` is a string and an email |
| **Semantic / business** (service) | rules needing data or context | email not already registered; quantity ≤ stock |
| **Database constraints** | last line of defense | `UNIQUE(email)`, `CHECK (quantity > 0)` |

DTO validation does not replace business-rule validation — it just makes sure the service receives well-formed input.

## Under the Hood: How Decorator Validation Finds Its Rules

`@IsEmail()` is a function that runs **once, when the class is defined** — not when a request arrives. It registers metadata `{ target: CreateUserDto, property: "email", constraint: "isEmail" }` into a global `MetadataStorage`. At request time `ValidationPipe`:

1. reads the parameter's design type (`CreateUserDto`) that TypeScript emitted via `emitDecoratorMetadata` / `reflect-metadata`,
2. calls `plainToInstance(CreateUserDto, body)` (class-transformer) to build an instance,
3. calls `validate(instance)`, which looks up every constraint registered for that class and runs them,
4. throws `BadRequestException` with the collected errors if any failed.

That's why DTOs must be **classes**, not interfaces or type aliases: interfaces are erased at compile time, leaving no run-time object to hang metadata on. Zod takes the opposite approach — the schema *is* a run-time object, and the type is derived from it with `z.infer`, so there's no reflection involved at all.

![Untrusted JSON enters a schema parser that either returns a normalized, typed DTO for the service or a 400 response listing every field error; unknown keys are stripped or rejected](diagrams/01-parse-dont-validate.svg)

## Common Pitfalls

1. **Casting instead of validating** (`req.body as Dto`) — compiles, checks nothing.
2. **Reusing the entity as the input DTO** — enables mass assignment of `role`, `id`, `balance`.
3. **Returning the entity directly** — leaks `passwordHash` and internal columns.
4. **Forgetting coercion for query/path params** — they are always strings; `"10" > "9"` is `false`.
5. **No size limits** — unbounded strings/arrays let a single request burn memory and CPU.
6. **Validating only on the client** — anyone can call the API directly with curl.

## Quick Reference

| Need | Zod | class-validator |
|---|---|---|
| define DTO | `z.object({...})` | class with decorators |
| static type | `z.infer<typeof S>` | the class itself |
| reject unknown keys | `.strict()` | `forbidNonWhitelisted: true` |
| strip unknown keys | default `.object()` (strips) | `whitelist: true` |
| coerce strings | `z.coerce.number()` | `transform: true` + `@Type(() => Number)` |
| check | `S.safeParse(x)` | `ValidationPipe` / `validate()` |
