Validación con Pipes

Intermedio
Nest
Nest
Actualizado: 05/05/2026

Diagrama: tutorial-nest-validation-pipes

Pipes existentes en Nest y cómo usarlos

Los pipes en NestJS son clases que implementan la interfaz PipeTransform y se ejecutan antes de que los datos lleguen al controlador. Su función principal es transformar los datos de entrada o validar que cumplan con ciertos criterios antes de procesarlos.

flowchart LR
    Request["Request body / param / query"] --> Decorator["@Body, @Param, @Query"]
    Decorator --> Pipe["PipeTransform.transform value, metadata"]
    Pipe --> Validate{"class-validator OK?"}
    Validate -->|No| BadReq["BadRequestException 400 + errors"]
    Validate -->|Si| Transform[class-transformer plainToClass]
    Transform --> ParseInt["ParseIntPipe / ParseUUIDPipe / ParseEnumPipe"]
    ParseInt --> Custom[Pipes custom específicos]
    Custom --> Handler[Handler del controller con DTO tipado]
    BadReq --> ClientError[Cliente recibe 400 con detalle]

El pipeline de pipes es la frontera entre el mundo unknown de la request HTTP y el mundo tipado del controller. Un ValidationPipe global con whitelist: true y forbidNonWhitelisted: true rechaza propiedades extra y nos da DTOs limpios listos para pasar al Service.

NestJS incluye varios pipes integrados que cubren las necesidades más comunes de validación y transformación. Estos pipes se pueden aplicar a nivel de parámetro, método o controlador, proporcionando flexibilidad en su implementación.

Pipes de validación integrados

ValidationPipe es el pipe más utilizado para validar objetos complejos usando decoradores de class-validator. Este pipe transforma automáticamente los objetos planos en instancias de clase y aplica las reglas de validación definidas:

import { Body, Controller, Post, UsePipes, ValidationPipe } from '@nestjs/common';
import { IsEmail, IsNotEmpty, MinLength } from 'class-validator';

class CreateUserDto {
  @IsNotEmpty()
  name: string;

  @IsEmail()
  email: string;

  @MinLength(6)
  password: string;
}

@Controller('users')
export class UsersController {
  @Post()
  @UsePipes(new ValidationPipe())
  create(@Body() createUserDto: CreateUserDto) {
    return `Usuario creado: ${createUserDto.name}`;
  }
}

ParseIntPipe convierte strings en números enteros y valida que la conversión sea exitosa. Es especialmente útil para parámetros de ruta que deben ser numéricos:

import { Controller, Get, Param, ParseIntPipe } from '@nestjs/common';

@Controller('users')
export class UsersController {
  @Get(':id')
  findOne(@Param('id', ParseIntPipe) id: number) {
    return `Usuario con ID: ${id}`;
  }
}

ParseBoolPipe transforma strings como "true" o "false" en valores booleanos reales:

import { Controller, Get, Query, ParseBoolPipe } from '@nestjs/common';

@Controller('products')
export class ProductsController {
  @Get()
  findAll(@Query('active', ParseBoolPipe) active: boolean) {
    return `Productos activos: ${active}`;
  }
}

Pipes de transformación de datos

ParseArrayPipe convierte strings separados por comas en arrays y puede aplicar transformaciones adicionales a cada elemento:

import { Controller, Get, Query, ParseArrayPipe } from '@nestjs/common';

@Controller('products')
export class ProductsController {
  @Get()
  findByIds(
    @Query('ids', new ParseArrayPipe({ items: Number, separator: ',' }))
    ids: number[]
  ) {
    return `Buscando productos: ${ids.join(', ')}`;
  }
}

ParseUUIDPipe valida que un string tenga formato UUID válido y puede especificar la versión requerida:

import { Controller, Get, Param, ParseUUIDPipe } from '@nestjs/common';

@Controller('orders')
export class OrdersController {
  @Get(':id')
  findOne(@Param('id', new ParseUUIDPipe({ version: '4' })) id: string) {
    return `Pedido con UUID: ${id}`;
  }
}

ParseEnumPipe valida que un valor pertenezca a un enum específico:

import { Controller, Get, Query, ParseEnumPipe } from '@nestjs/common';

enum OrderStatus {
  PENDING = 'pending',
  COMPLETED = 'completed',
  CANCELLED = 'cancelled'
}

@Controller('orders')
export class OrdersController {
  @Get()
  findByStatus(
    @Query('status', new ParseEnumPipe(OrderStatus))
    status: OrderStatus
  ) {
    return `Pedidos con estado: ${status}`;
  }
}

Aplicación de pipes a diferentes niveles

Los pipes se pueden aplicar de múltiples formas según el alcance deseado. A nivel de parámetro, el pipe solo afecta a ese parámetro específico:

@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number) {
  return `Usuario: ${id}`;
}

A nivel de método, el pipe se aplica a todos los parámetros del método:

@Get(':id')
@UsePipes(ValidationPipe)
findOne(@Param('id') id: string, @Body() updateDto: UpdateUserDto) {
  return 'Método con pipe aplicado';
}

A nivel de controlador, el pipe afecta a todos los métodos del controlador:

@Controller('users')
@UsePipes(new ValidationPipe({ whitelist: true }))
export class UsersController {
  // Todos los métodos heredan el ValidationPipe
}

Configuración avanzada de pipes

Los pipes integrados aceptan opciones de configuración que modifican su comportamiento. El ValidationPipe, por ejemplo, puede configurarse para eliminar propiedades no definidas en el DTO:

@Post()
@UsePipes(new ValidationPipe({
  whitelist: true,           // Elimina propiedades no definidas en el DTO
  forbidNonWhitelisted: true, // Lanza error si hay propiedades extra
  transform: true            // Transforma tipos automáticamente
}))
create(@Body() createUserDto: CreateUserDto) {
  return 'Usuario creado con validación estricta';
}

Para manejo de errores personalizado, los pipes pueden configurarse con mensajes específicos:

@Get(':id')
findOne(
  @Param('id', new ParseIntPipe({
    errorHttpStatusCode: HttpStatus.NOT_ACCEPTABLE
  })) 
  id: number
) {
  return `Usuario: ${id}`;
}

Los pipes también pueden combinarse para aplicar múltiples transformaciones y validaciones en secuencia:

@Get(':id')
findOne(
  @Param('id', ParseIntPipe, new DefaultValuePipe(1)) 
  id: number
) {
  return `Usuario: ${id}`;
}

Esta flexibilidad permite crear flujos de validación robustos que garantizan que los datos lleguen al controlador en el formate correcto y cumpliendo todas las reglas de negocio establecidas.

Pipes personalizados

Cuando los pipes integrados no cubren un caso, se crea uno propio implementando PipeTransform:

import { PipeTransform, Injectable, BadRequestException } from '@nestjs/common';

@Injectable()
export class TrimPipe implements PipeTransform<string, string> {
  transform(value: string): string {
    if (typeof value !== 'string') {
      throw new BadRequestException('Se esperaba un string');
    }
    return value.trim().replace(/\s+/g, ' ');
  }
}

@Get(':slug')
findBySlug(@Param('slug', TrimPipe) slug: string) {
  return this.service.findBySlug(slug);
}

Pipes con dependencias inyectables permiten validar contra base de datos, por ejemplo verificar que un email no esté ya registrado:

@Injectable()
export class UniqueEmailPipe implements PipeTransform<CreateUserDto, Promise<CreateUserDto>> {
  constructor(@InjectRepository(User) private readonly repo: Repository<User>) {}

  async transform(value: CreateUserDto): Promise<CreateUserDto> {
    const exists = await this.repo.exists({ where: { email: value.email } });
    if (exists) throw new ConflictException('Email ya registrado');
    return value;
  }
}

Configuración global recomendada para producción

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(
    new ValidationPipe({
      whitelist: true,
      forbidNonWhitelisted: true,
      transform: true,
      transformOptions: { enableImplicitConversion: true },
      stopAtFirstError: false,
      exceptionFactory: (errors) => {
        const detail = errors.flatMap((e) =>
          Object.values(e.constraints ?? {}),
        );
        return new BadRequestException({
          type: 'https://api.example.com/errors/validation',
          title: 'Datos de entrada inválidos',
          status: 400,
          detail,
        });
      },
    }),
  );
  await app.listen(3000);
}
  • whitelist + forbidNonWhitelisted evita inyecciones tipo "envío campos extra que el DTO no declara".
  • transform: true convierte el body plano en una instancia real del DTO, necesario para que TypeORM valide los tipos antes de persistir.
  • enableImplicitConversion: true permite que strings como "30" se conviertan a number cuando el campo es @IsInt().
  • exceptionFactory da control total sobre el formato del error 400 (Problem Details RFC 7807, mensajes localizados, códigos de negocio).

Decoradores avanzados de class-validator

class-validator incluye decoradores potentes que cubren casos reales:

import {
  IsString, IsEmail, MinLength, MaxLength, Matches,
  IsArray, ArrayMinSize, ArrayMaxSize, ValidateNested,
  IsDate, IsOptional, IsIn, IsObject, IsPhoneNumber,
} from 'class-validator';
import { Type } from 'class-transformer';

export class AddressDto {
  @IsString() @IsNotEmpty() street: string;
  @IsString() city: string;
  @Matches(/^[0-9]{5}$/) postalCode: string;
}

export class CreateUserDto {
  @IsEmail() email: string;

  @IsString()
  @MinLength(8)
  @MaxLength(72)
  @Matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).+$/, {
    message: 'La contraseña requiere mayúscula, minúscula y dígito',
  })
  password: string;

  @IsIn(['user', 'admin', 'manager'])
  role: string;

  @IsPhoneNumber('ES')
  @IsOptional()
  phone?: string;

  @IsArray()
  @ArrayMinSize(1)
  @ArrayMaxSize(10)
  @ValidateNested({ each: true })
  @Type(() => AddressDto)
  addresses: AddressDto[];
}

@ValidateNested con @Type activa la validación recursiva sobre objetos anidados. Sin @Type el class-transformer no sabría qué clase usar y los decoradores hijos quedarían inactivos.

Validación en tests

Para tests unitarios del DTO sin levantar la app:

import { plainToInstance } from 'class-transformer';
import { validate } from 'class-validator';

it('rechaza email mal formado', async () => {
  const dto = plainToInstance(CreateUserDto, {
    email: 'no-es-email',
    password: 'Aa1aaaaaaa',
    role: 'user',
    addresses: [{ street: 'X', city: 'Madrid', postalCode: '28001' }],
  });
  const errors = await validate(dto);
  expect(errors).toHaveLength(1);
  expect(errors[0].property).toBe('email');
});

Esto permite validar reglas complejas con cobertura quirúrgica antes de probar el endpoint completo en e2e.

Fuentes y referencias

Documentación oficial y recursos externos para profundizar en Nest

Documentación oficial de Nest
Alan Sastre - Autor del tutorial

Alan Sastre

Ingeniero de Software y formador, CEO en CertiDevs

Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, Nest es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.

Más tutoriales de Nest

Explora más contenido relacionado con Nest y continúa aprendiendo con nuestros tutoriales gratuitos.

Aprendizajes de esta lección

Comprender qué son los pipes en NestJS y su función principal. Aprender a utilizar los pipes integrados para validación y transformación de datos. Aplicar pipes a diferentes niveles: parámetro, método y controlador. Configurar opciones avanzadas de los pipes para personalizar su comportamiento. Combinar múltiples pipes para crear flujos de validación robustos.