DTOs en Nest JS

Posted on October 31, 2025
Profile
Gastón Gaitan
October 31, 2025 · 5 days, 14 hours ago
DTOs en Nest JS

¿Qué son los DTOs y por qué son necesarios en NestJS?

¿Qué es un DTO?

DTO significa Data Transfer Object (Objeto de Transferencia de Datos). Es un patrón de diseño que define la estructura y validación de los datos que se transfieren entre diferentes capas de una aplicación, especialmente entre el cliente y el servidor.

En NestJS, los DTOs son clases de TypeScript que no solo definen la estructura esperada de los datos, sino que también validan automáticamente que los datos recibidos cumplan con las reglas definidas antes de que lleguen a tu lógica de negocio.

Diagrama de Data Transfer Object (DTO) entre Cliente y Servidor
Diagrama que muestra cómo un DTO actúa como objeto de transferencia entre el Cliente (caja teal/turquesa) y el Servidor (caja verde claro). La caja amarilla "DTO" sobre la flecha indica que es el objeto que se transfiere.

El problema que resuelven los DTOs

Imagina que tienes un endpoint para crear usuarios. Sin DTOs, tendrías que escribir código manual para validar cada campo:

@Post('/users')
async createUser(@Body() body: any) {
  // ❌ Validación manual (propenso a errores)
  if (!body.email || !this.isValidEmail(body.email)) {
    throw new BadRequestException('Email inválido');
  }
  if (!body.first_name || typeof body.first_name !== 'string') {
    throw new BadRequestException('Nombre inválido');
  }
  // ... más validaciones manuales ...
  
  return this.userService.create(body);
}

Este enfoque tiene varios problemas:

  • Código repetitivo: Tienes que validar manualmente en cada endpoint
  • Propenso a errores: Puedes olvidar validar algún campo
  • Difícil de mantener: Si cambias las reglas, debes modificar múltiples lugares
  • Sin documentación automática: No se genera documentación Swagger automáticamente

La solución: DTOs con decoradores

Con DTOs, defines una vez las reglas de validación y NestJS las aplica automáticamente:

import { IsEmail, IsNotEmpty, IsString, IsOptional } from 'class-validator';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';

export class CreateUserDto {
  @ApiProperty({ 
    description: 'User email address', 
    example: 'user@example.com' 
  })
  @IsEmail()
  @IsNotEmpty()
  email: string;

  @ApiProperty({ 
    description: 'User first name', 
    example: 'John' 
  })
  @IsString()
  @IsNotEmpty()
  first_name: string;

  @ApiPropertyOptional({ 
    description: 'User phone number', 
    example: '+1234567890' 
  })
  @IsOptional()
  @IsPhoneNumber()
  phone?: string;
}

Luego, en tu controlador simplemente usas el DTO:

@Post('/users')
async createUser(@Body() userDto: CreateUserDto) {
  // ✅ Los datos ya están validados automáticamente
  // Si los datos son inválidos, NestJS rechaza la request antes de llegar aquí
  return this.userService.create(userDto);
}

Si alguien envía datos inválidos, NestJS automáticamente responde con un error 400:

// Request inválido
POST /users
{
  "email": "not-an-email",
  "first_name": 123
}

// Respuesta automática
{
  "statusCode": 400,
  "message": [
    "email must be an email",
    "first_name must be a string"
  ],
  "error": "Bad Request"
}

Componentes de un DTO en NestJS

Un DTO completo en NestJS combina tres bibliotecas poderosas:

1. class-validator: Validación de datos

Define las reglas de validación que se ejecutan en tiempo de ejecución:

@IsEmail()           // Valida formato de email
@IsNotEmpty()        // Campo requerido
@IsString()          // Debe ser string
@IsOptional()        // Campo opcional
@IsNumber()          // Debe ser número
@IsBoolean()          // Debe ser booleano
@IsPhoneNumber()     // Valida formato de teléfono
@Length(1, 50)       // Longitud mínima y máxima
@IsPositive()        // Número positivo
@IsEnum(OrderStatus) // Valor de un enum específico

2. class-transformer: Transformación de datos

Permite transformar los datos antes de la validación:

@Transform(({ value }) => {
  // Convierte string "1,2,3" a array [1, 2, 3]
  if (typeof value === 'string') {
    return value.split(',').map(id => parseInt(id.trim(), 10));
  }
  return value;
})
@IsArray()
@IsNumber({}, { each: true })
brand_ids?: number[];

Esto es especialmente útil cuando recibes datos de formularios HTML o APIs que envían todo como strings.

3. @nestjs/swagger: Documentación automática

Genera automáticamente la documentación de la API en Swagger:

@ApiProperty({ 
  description: 'User email address', 
  example: 'user@example.com',
  type: String
})
@IsEmail()
email: string;

Con estos decoradores, Swagger UI muestra automáticamente:

  • Qué campos son requeridos
  • Qué tipo de datos se esperan
  • Ejemplos de valores válidos
  • Descripciones de cada campo

DTOs vs Interfaces: ¿Cuándo usar cada uno?

Es común confundir DTOs con interfaces de TypeScript. Aunque ambas definen estructuras de datos, tienen propósitos muy diferentes:

Característica DTO (Clase) Interface
Validación en runtime ✅ Sí ❌ No
Transformación de datos ✅ Sí ❌ No
Documentación Swagger ✅ Sí ❌ No
Existe en runtime ✅ Sí ❌ No (solo compilación)
Mejor para Datos de entrada (requests HTTP) Datos internos, resultados de funciones

Ejemplo práctico:

// ✅ DTO: Para datos de entrada que necesitan validación
export class CreateUserDto {
  @IsEmail()
  email: string;
}

// ✅ Interface: Para datos internos que ya están validados
export interface UserInfo {
  email: string;
  name: string;
}

// En el controlador
@Post('/users')
async create(@Body() userDto: CreateUserDto) { // DTO valida automáticamente
  const userInfo: UserInfo = await this.authService.getUserInfo(); // Interface solo tipa
  return userInfo;
}

Ejemplo completo: DTO con validación avanzada

Aquí tienes un ejemplo real de un DTO completo que muestra las capacidades avanzadas:

export class CreateUserDto {
  // Campo requerido con validación de email
  @ApiProperty({ 
    description: 'User email address', 
    example: 'user@example.com' 
  })
  @IsEmail()
  @IsNotEmpty()
  email: string;

  // Campo requerido
  @ApiProperty({ 
    description: 'User first name', 
    example: 'John' 
  })
  @IsString()
  @IsNotEmpty()
  first_name: string;

  // Campo opcional con transformación
  @ApiPropertyOptional({ 
    description: 'List of brand IDs. Can be array [1,2,3] or string "1,2,3"', 
    example: [1, 2, 3]
  })
  @IsOptional()
  @Transform(({ value }) => {
    // Transforma string "1,2,3" a array [1, 2, 3]
    if (typeof value === 'string') {
      return value.split(',')
        .map(id => parseInt(id.trim(), 10))
        .filter(id => !isNaN(id));
    }
    return value;
  })
  @IsArray()
  @IsNumber({}, { each: true })
  brand_ids?: number[] | string;
}

Este DTO:

  1. Valida que el email sea válido y requerido
  2. Valida que el nombre sea string y requerido
  3. Transforma strings como "1,2,3" a arrays [1, 2, 3]
  4. Valida que brand_ids sea un array de números
  5. Documenta automáticamente en Swagger

¿Por qué son necesarios los DTOs?

Los DTOs son esenciales por varias razones:

1. Seguridad

Validan y sanitizan los datos antes de que lleguen a tu lógica de negocio, previniendo inyecciones SQL, XSS y otros ataques comunes.

2. Confiabilidad

Garantizan que siempre trabajas con datos en el formato correcto, reduciendo errores en tiempo de ejecución y bugs difíciles de encontrar.

3. Mantenibilidad

Centralizan las reglas de validación en un solo lugar, haciendo el código más fácil de mantener y actualizar.

4. Documentación automática

Generan automáticamente documentación Swagger completa y siempre actualizada, mejorando la experiencia del desarrollador y facilitando la integración.

5. Desarrollo más rápido

Con validación automática, puedes confiar en que los datos están correctos y enfocarte en escribir la lógica de negocio.

6. Experiencia de desarrollador

El autocompletado y el tipado fuerte de TypeScript funcionan perfectamente con DTOs, ayudándote a escribir código más rápido y con menos errores.

Conclusión

Los DTOs son mucho más que simples definiciones de tipos. Son una capa esencial de seguridad, validación y documentación que hace que tus APIs sean más robustas, mantenibles y fáciles de usar.

En NestJS, los DTOs se integran perfectamente con el framework, proporcionando validación automática, transformación de datos y documentación Swagger sin esfuerzo adicional. Invertir tiempo en diseñar buenos DTOs es una de las mejores prácticas que puedes adoptar al desarrollar APIs backend.

La próxima vez que crees un endpoint, recuerda: si recibes datos del cliente, usa un DTO. Tu código será más seguro, más limpio y más fácil de mantener.