¿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.
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:
- Valida que el email sea válido y requerido
- Valida que el nombre sea string y requerido
- Transforma strings como "1,2,3" a arrays [1, 2, 3]
- Valida que brand_ids sea un array de números
- 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.