En esta página

Pipes y validación de datos

14 min lectura TextoCap. 2 — Servicios e inyección

¿Qué son los pipes en NestJS?

Los pipes son clases que implementan la interfaz PipeTransform y se ejecutan antes de que el método del controlador reciba los datos. Tienen dos propósitos principales:

  1. Transformación: Convertir los datos de entrada a la forma deseada (string a número, string a fecha, plainObject a instancia de clase, etc.)
  2. Validación: Evaluar los datos de entrada y lanzar una excepción si no son válidos

Los pipes son una capa defensiva que garantiza que tu lógica de negocio nunca recibe datos malformados o inesperados.

Pipes integrados de NestJS

NestJS incluye varios pipes listos para usar en el paquete @nestjs/common:

import {
  ValidationPipe,      // valida con class-validator
  ParseIntPipe,        // convierte string a número entero
  ParseFloatPipe,      // convierte string a número decimal
  ParseBoolPipe,       // convierte 'true'/'false' a boolean
  ParseUUIDPipe,       // valida formato UUID
  ParseEnumPipe,       // valida que el valor esté en un enum
  ParseArrayPipe,      // convierte strings separados por coma a array
  DefaultValuePipe,    // proporciona un valor por defecto si es null/undefined
} from '@nestjs/common';

Ejemplos de uso

@Controller('articulos')
export class ArticulosController {

  // ParseIntPipe — lanza BadRequestException si no es un número válido
  @Get(':id')
  findOne(@Param('id', ParseIntPipe) id: number) {
    return this.service.findOne(id);
  }

  // ParseUUIDPipe — valida el formato UUID
  @Get('uuid/:uuid')
  findByUuid(@Param('uuid', ParseUUIDPipe) uuid: string) {
    return this.service.findByUuid(uuid);
  }

  // DefaultValuePipe + ParseIntPipe combinados
  @Get()
  findAll(
    @Query('pagina', new DefaultValuePipe(1), ParseIntPipe) pagina: number,
    @Query('limite', new DefaultValuePipe(10), ParseIntPipe) limite: number,
  ) {
    return this.service.findAll({ pagina, limite });
  }

  // ParseEnumPipe — valida que el valor esté en el enum
  @Get('estado/:estado')
  findByEstado(
    @Param('estado', new ParseEnumPipe(EstadoArticulo)) estado: EstadoArticulo,
  ) {
    return this.service.findByEstado(estado);
  }
}

class-validator — La librería de validación

class-validator es la librería estándar de validación para NestJS. Usa decoradores en las clases DTO para declarar las reglas de validación:

npm install class-validator class-transformer

Los decoradores más utilizados son:

// Strings
@IsString()        // es string
@IsNotEmpty()      // no está vacío (ni '')
@MinLength(3)      // longitud mínima
@MaxLength(100)    // longitud máxima
@Matches(/regex/)  // debe coincidir con la expresión regular

// Números
@IsNumber()        // es número
@IsInt()           // es entero
@Min(0)            // valor mínimo
@Max(100)          // valor máximo
@IsPositive()      // mayor que 0

// Colecciones
@IsArray()         // es array
@ArrayMinSize(1)   // tamaño mínimo del array
@ArrayMaxSize(10)  // tamaño máximo
@IsIn(['a', 'b'])  // el valor está en la lista

// Booleanos y enums
@IsBoolean()       // es booleano
@IsEnum(MiEnum)    // el valor está en el enum

// Formato especial
@IsEmail()         // formato de email
@IsUrl()           // URL válida
@IsUUID()          // UUID válido
@IsISO8601()       // fecha ISO 8601 válida

// Opcionalidad
@IsOptional()      // el campo puede estar ausente o ser null/undefined

class-transformer — Transformación de datos

class-transformer complementa a class-validator con decoradores de transformación:

import { Transform, Type, Exclude, Expose } from 'class-transformer';

export class UsuarioDto {
  @Transform(({ value }: { value: string }) => value.trim().toLowerCase())
  email: string;

  @Type(() => Number)  // convierte string a número durante la deserialización
  edad: number;

  @Exclude()  // excluye este campo de la serialización
  password: string;
}

El decorador @Exclude() es especialmente útil para evitar que campos sensibles (contraseñas, tokens) aparezcan en las respuestas JSON.

ValidationPipe — Configuración avanzada

El ValidationPipe es el pipe que conecta class-validator y class-transformer con NestJS:

new ValidationPipe({
  whitelist: true,              // elimina propiedades no declaradas en el DTO
  forbidNonWhitelisted: true,   // lanza error 400 si hay propiedades extra
  transform: true,              // transforma el payload al tipo del DTO
  disableErrorMessages: false,  // en producción, podrías ocultar mensajes detallados
  validationError: {
    target: false,              // no incluye el objeto validado en el error
    value: false,               // no incluye el valor inválido en el error
  },
  exceptionFactory: (errors) => {
    // Personaliza el formato de los errores de validación
    const mensajes = errors.map(err =>
      Object.values(err.constraints ?? {}).join(', ')
    );
    return new BadRequestException({
      statusCode: 400,
      mensaje: 'Error de validación',
      errores: mensajes,
    });
  },
})

Validación de arrays anidados

Para validar objetos anidados y arrays de objetos, usa @ValidateNested y @Type:

import { ValidateNested, IsArray } from 'class-validator';
import { Type } from 'class-transformer';

export class DireccionDto {
  @IsString()
  calle: string;

  @IsString()
  ciudad: string;
}

export class CrearPedidoDto {
  @ValidateNested()
  @Type(() => DireccionDto)
  direccion: DireccionDto;

  @IsArray()
  @ValidateNested({ each: true })
  @Type(() => ItemPedidoDto)
  items: ItemPedidoDto[];
}

Crear un pipe personalizado

Cuando los pipes integrados no son suficientes, puedes crear el tuyo propio implementando PipeTransform:

// pipes/slugify.pipe.ts
import { PipeTransform, Injectable } from '@nestjs/common';

@Injectable()
export class SlugifyPipe implements PipeTransform<string, string> {
  transform(value: string): string {
    return value
      .toLowerCase()
      .normalize('NFD')                    // descompone caracteres acentuados
      .replace(/[\u0300-\u036f]/g, '')    // elimina los diacríticos
      .replace(/[^a-z0-9\s-]/g, '')       // elimina caracteres especiales
      .trim()
      .replace(/[\s_-]+/g, '-')           // convierte espacios a guiones
      .replace(/^-+|-+$/g, '');           // elimina guiones al inicio/final
  }
}

// Uso:
@Post()
create(@Body('titulo', SlugifyPipe) slug: string) {
  return { slug }; // 'Hola Mundo' → 'hola-mundo'
}

Alcance de los pipes

Al igual que los guards e interceptores, los pipes se pueden aplicar en varios niveles:

// Nivel de parámetro (más granular)
@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number) {}

// Nivel de método
@Post()
@UsePipes(new ValidationPipe())
create(@Body() dto: CrearArticuloDto) {}

// Nivel de controlador
@Controller('productos')
@UsePipes(ValidationPipe)
export class ProductosController {}

// Nivel global (en main.ts) — aplicado a todos los controladores
app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true }));

La validación sólida de datos de entrada es una de las primeras líneas de defensa de cualquier API. En la siguiente lección aprenderemos a implementar la segunda línea de defensa: los guards de autorización.

whitelist: true es esencial en producción
Siempre activa `whitelist: true` en el `ValidationPipe` global. Esta opción elimina automáticamente cualquier propiedad que no esté declarada en el DTO, protegiéndote de ataques de mass assignment donde un usuario malintencionado podría enviar propiedades como `rol: 'admin'` en el cuerpo de la petición.
transform: true y los tipos en los DTOs
Cuando activas `transform: true`, NestJS intenta transformar el cuerpo de la petición al tipo declarado en el parámetro del controlador. Sin embargo, los valores de `@Query()` y `@Param()` siempre llegan como strings. Usa `@Type(() => Number)` de class-transformer en los campos numéricos de tus DTOs para que la conversión funcione correctamente.