Lesson 17 of 49 intermediate

Validation & DTOs

Never Trust User Input

Open interactive version (quiz + challenge)

Real-world analogy

DTOs are like airport security checkpoints. Before you board a plane (enter the server), your luggage (data) goes through scanners (validators). Is your bag the right size? Any prohibited items? If anything is wrong, you get stopped BEFORE you reach the plane. Without security (validation), anyone could bring anything on board — chaos!

What is it?

DTOs (Data Transfer Objects) define the expected shape of request data. Combined with class-validator, they automatically validate incoming data — checking types, formats, lengths, and custom rules. NestJS ValidationPipe connects DTOs to the request pipeline, rejecting invalid data with helpful error messages before it reaches your business logic.

Real-world relevance

Every form submission, API call, and webhook payload should be validated. Without validation, users could send malformed data that crashes your database, inject malicious code, or bypass business rules. DTOs are your first line of defense.

Key points

Code example

// Install: npm install class-validator class-transformer

// create-user.dto.ts
import {
  IsString, IsEmail, MinLength,
  IsOptional, IsEnum, IsNotEmpty
} from 'class-validator';

enum UserRole { ADMIN = 'admin', USER = 'user' }

export class CreateUserDTO {
  @IsString()
  @IsNotEmpty()
  @MinLength(2)
  name: string;

  @IsEmail()
  email: string;

  @IsString()
  @MinLength(8)
  password: string;

  @IsOptional()
  @IsEnum(UserRole)
  role?: UserRole;
}

// update-user.dto.ts — all fields optional!
import { PartialType } from '@nestjs/mapped-types';
export class UpdateUserDTO extends PartialType(CreateUserDTO) {}

// user.controller.ts
import { Body, Post, Put, Param } from '@nestjs/common';

@Post()
async create(@Body() dto: CreateUserDTO) {
  // dto is already validated! Safe to use.
  return this.userService.create(dto);
}

@Put(':id')
async update(@Param('id') id: string, @Body() dto: UpdateUserDTO) {
  return this.userService.update(id, dto);
}

// main.ts — Enable validation globally
import { ValidationPipe } from '@nestjs/common';

const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe({
  whitelist: true,    // strip unknown properties
  transform: true,    // auto-transform types
  forbidNonWhitelisted: true, // error on unknown props
}));

// What happens when invalid data is sent:
// POST /users { name: "", email: "not-email", hack: "injected" }
// Response 400:
// {
//   "statusCode": 400,
//   "message": [
//     "name must be longer than or equal to 2 characters",
//     "email must be an email",
//     "property hack should not exist"
//   ],
//   "error": "Bad Request"
// }

Line-by-line walkthrough

  1. 1. Install: npm install class-validator class-transformer
  2. 2.
  3. 3. create-user.dto.ts
  4. 4. Importing required dependencies
  5. 5.
  6. 6.
  7. 7.
  8. 8.
  9. 9. Defining an enum
  10. 10.
  11. 11. Exporting for use in other files
  12. 12. Decorator that adds metadata or behavior
  13. 13. Decorator that adds metadata or behavior
  14. 14. Decorator that adds metadata or behavior
  15. 15.
  16. 16.
  17. 17. Decorator that adds metadata or behavior
  18. 18.
  19. 19.
  20. 20. Decorator that adds metadata or behavior
  21. 21. Decorator that adds metadata or behavior
  22. 22.
  23. 23.
  24. 24. Decorator that adds metadata or behavior
  25. 25. Decorator that adds metadata or behavior
  26. 26.
  27. 27. Closing block
  28. 28.
  29. 29. update-user.dto.ts — all fields optional!
  30. 30. Importing required dependencies
  31. 31. Exporting for use in other files
  32. 32.
  33. 33. user.controller.ts
  34. 34. Importing required dependencies
  35. 35.
  36. 36. Decorator that adds metadata or behavior
  37. 37.
  38. 38. dto is already validated! Safe to use.
  39. 39. Returning a value
  40. 40. Closing block
  41. 41.
  42. 42. Decorator that adds metadata or behavior
  43. 43.
  44. 44. Returning a value
  45. 45. Closing block
  46. 46.
  47. 47. main.ts — Enable validation globally
  48. 48. Importing required dependencies
  49. 49.
  50. 50. Declaring a variable
  51. 51.
  52. 52.
  53. 53.
  54. 54.
  55. 55.
  56. 56.
  57. 57. What happens when invalid data is sent:
  58. 58. POST /users { name: "", email: "not-email", hack: "injected" }
  59. 59. Response 400:
  60. 60. {
  61. 61. "statusCode": 400,
  62. 62. "message": [
  63. 63. "name must be longer than or equal to 2 characters",
  64. 64. "email must be an email",
  65. 65. "property hack should not exist"
  66. 66. ],
  67. 67. "error": "Bad Request"
  68. 68. }

Spot the bug

export class CreateUserDTO {
  @IsString()
  name: string;

  @IsEmail()
  email: string;

  @MinLength(8)
  password: string;
}
Need a hint?
What type decorator is missing from the password field?
Show answer
Password has @MinLength(8) but is missing @IsString() decorator. Best practice: always include the type decorator. Fix: add @IsString() above @MinLength(8).

Explain like I'm 5

Imagine filling out a form to join a club. The form says 'Name: at least 2 letters' and 'Age: must be a number'. If you write something wrong, the form tells you what to fix BEFORE you hand it in. DTOs are like those smart forms that check your answers!

Fun fact

The 2017 Equifax data breach exposed 147 million people's personal data. The root cause? A known vulnerability in an unpatched server — but proper input validation and security layers could have limited the damage significantly.

Hands-on challenge

Create a CreateProductDTO with: name (string, min 3 chars), price (number, positive), category (enum: electronics/clothing/food), description (optional string, max 500 chars). Set up ValidationPipe globally and test with invalid data!

More resources

Open interactive version (quiz + challenge) ← Back to course: Full-Stack Playbook