Cuando desarrollamos aplicaciones con NestJS, una de las prácticas más importantes es la validación de los datos que nuestras APIs reciben. Validar adecuadamente los datos no solo mejora la seguridad de la aplicación, sino que también garantiza que los datos sean coherentes y estén correctamente formateados antes de ser procesados por las capas de negocio.
En este artículo, exploraremos cómo realizar la validación de sintaxis y semántica en NestJS, utilizando Data Transfer Objects (DTOs), class-validator
, y servicios. También veremos cómo la transformación de datos en los controladores puede ser una herramienta útil y cómo estas prácticas pueden hacer que nuestro código sea más robusto y seguro.
1. Validación de Sintaxis con DTOs y class-validator
La validación de sintaxis se centra en asegurarse de que los datos que recibimos cumplen con el formato y tipo de datos esperados. En NestJS, esto se logra utilizando DTOs (Data Transfer Objects
) junto con la biblioteca class-validator
.
1.1 ¿Qué es un DTO?
Un DTO es una clase que define la estructura de los datos que se esperan recibir. NestJS utiliza estas clases para validar automáticamente los datos de entrada antes de que lleguen a la lógica de negocio. Aquí hay un ejemplo básico de un DTO:
import { IsString, IsInt, Min, IsEmail } from 'class-validator'; export class CreateUserDto { @IsString() readonly name: string; @IsInt() @Min(0) readonly age: number; @IsEmail() readonly email: string;
}
Este DTO asegura que, al crear un nuevo usuario, el nombre sea una cadena, la edad sea un número entero y que el correo electrónico tenga un formato válido.
1.2 Validación Automática con class-validator
Cuando usamos class-validator
, podemos aplicar diferentes decoradores a las propiedades del DTO para definir las reglas de validación. Algunas de las más comunes incluyen:
-
@IsString()
: Valida que el valor sea una cadena. -
@IsInt()
: Valida que el valor sea un número entero. -
@Min(valor)
: Valida que el valor sea mayor o igual al valor mínimo especificado. -
@IsEmail()
: Valida que el valor sea un correo electrónico válido.
Estos decoradores permiten definir reglas de sintaxis claras y precisas, garantizando que cualquier dato que no cumpla con estos criterios sea rechazado automáticamente antes de llegar al servicio.
1.3 Custom Pipes para Validaciones Adicionales
En ocasiones, es necesario aplicar validaciones más complejas o personalizadas, como asegurarse de que un valor pertenezca a un conjunto específico de valores. Para esto, podemos usar pipes personalizados.
Ejemplo de un pipe personalizado para validar un enum:
import { PipeTransform, BadRequestException } from '@nestjs/common'; export class ParseEnumPipe implements PipeTransform<string, any> { constructor(private readonly enumType: object) {} transform(value: string): any { if (!(value in this.enumType)) { throw new BadRequestException(`${value} is not a valid option`); } return this.enumType[value]; }
}
Uso del pipe en un controlador:
@Post()
createTask(@Body('status', new ParseEnumPipe(TaskStatus)) status: TaskStatus) { return this.taskService.createTask(status);
}
Aquí, ParseEnumPipe
se asegura de que el valor de status
pertenezca a los valores permitidos por el enum TaskStatus
. Esta validación adicional ayuda a mantener la integridad de los datos desde las etapas más tempranas del procesamiento.
2. Validación de Semántica en los Servicios: Garantizando la Coherencia del Negocio
La validación de semántica se realiza en los servicios, donde se verifica que los datos no solo estén correctamente formateados, sino que también tengan sentido dentro del contexto de la aplicación. Este tipo de validación asegura que los datos cumplan con las reglas de negocio antes de que se realice cualquier operación, como la inserción o actualización en la base de datos.
2.1 Ejemplo de Validación Semántica en un Servicio
Supongamos que estamos trabajando con órdenes de compra. Queremos asegurarnos de que una orden exista y que esté en un estado válido (PENDING
) antes de confirmarla. Esta validación se realiza en el servicio:
@Injectable()
export class OrderService { async validateOrder(orderId: string): Promise<Order> { const order = await this.orderRepository.findOne(orderId); if (!order) { throw new NotFoundException(`Order with ID ${orderId} not found`); } if (order.status !== 'PENDING') { throw new BadRequestException('Order is not in a valid state'); } return order; } async confirmOrder(order: Order): Promise<Order> { order.status = 'CONFIRMED'; return this.orderRepository.save(order); }
}
En este ejemplo, el método validateOrder
se asegura de que la orden cumpla con todas las condiciones necesarias antes de que se realice cualquier operación en la base de datos. Este tipo de validación protege contra errores y asegura que solo se procesen datos válidos.
3. Transformación de Datos en los Controladores: Manejando los Datos para la Coherencia
En algunos casos, es necesario transformar los datos antes de que lleguen a los servicios o después de recibir la respuesta. Esto puede incluir convertir datos de un formato a otro, agregar o eliminar propiedades, o realizar cálculos adicionales.
3.1 ¿Por qué y cuándo transformar los datos?
Transformar los datos puede ser necesario para mantener la coherencia entre las diferentes capas de la aplicación o para cumplir con las expectativas de la interfaz de usuario o las API externas. Algunos escenarios comunes donde es útil transformar datos incluyen:
- Normalización de datos: Convertir los datos a un formato estándar.
@Post() createUser(@Body() createUserDto: CreateUserDto) { const normalizedDto = { ...createUserDto, email: createUserDto.email.toLowerCase(), }; return this.userService.createUser(normalizedDto); }
- Agregar información adicional: Enriquecer los datos con información adicional requerida por los servicios.
@Post(':orderId/items') addItemToOrder( @Param('orderId') orderId: string, @Body() addItemDto: AddItemDto, ) { const enhancedDto = { ...addItemDto, orderId, }; return this.orderService.addItemToOrder(enhancedDto); }
- Transformación de formatos: Cambiar el formato de los datos para cumplir con los requisitos de un servicio o API.
@Post() createReport(@Body() reportDto: ReportDto) { const transformedDto = { ...reportDto, date: new Date(reportDto.date).toISOString(), }; return this.reportService.createReport(transformedDto); }
3.2 Uso de pipes para transformar datos
Los pipes no solo son útiles para la validación, sino también para la transformación de datos. Puedes crear pipes personalizados que realicen transformaciones antes de que los datos lleguen al controlador.
Ejemplo de un pipe para transformar una fecha:
import { PipeTransform, Injectable, BadRequestException } from '@nestjs/common';
import { isDate, parseISO } from 'date-fns'; @Injectable()
export class ParseDatePipe implements PipeTransform<string, Date> { transform(value: string): Date { const date = parseISO(value); if (!isDate(date)) { throw new BadRequestException('Invalid date format'); } return date; }
}
Este pipe convierte una cadena en una instancia de Date
y se asegura de que sea válida.
Utilizando el pipe en un controlador:
@Post()
createEvent(@Body('date', ParseDatePipe) date: Date) { return this.eventService.createEvent({ date });
}
Aquí, ParseDatePipe
transforma la cadena de fecha antes de que llegue al servicio eventService
.
4. Validación de Sintaxis y Semántica: Garantizando un Código Robusto y Seguro
Es crucial validar tanto la sintaxis como la semántica de los datos recibidos antes de realizar cualquier operación que modifique la base de datos, como insertar o actualizar registros. Este enfoque no solo asegura la integridad de los datos, sino que también fortalece la robustez y seguridad del código.
4.1 Validación de Sintaxis con DTOs y class-validator
La validación de sintaxis se lleva a cabo utilizando DTOs (Data Transfer Objects
) junto con el paquete class-validator
. Esto garantiza que los datos cumplan con las reglas de formato y tipos de datos esperados antes de que lleguen al servicio.
import { IsString, IsInt, Min, IsEmail } from 'class-validator'; export class CreateUserDto { @IsString() readonly name: string; @IsInt() @Min(0) readonly age: number; @IsEmail() readonly email: string;
}
Con esta validación, cualquier dato
que no cumpla con los requisitos especificados en el DTO será rechazado automáticamente, protegiendo así la integridad del sistema.
4.2 Validación Semántica en los Servicios
La validación semántica ocurre en la capa de servicios, donde se aseguran que los datos no solo estén correctamente formateados, sino que también sean coherentes dentro del contexto de negocio.
@Injectable()
export class OrderService { async validateOrder(orderId: string): Promise<Order> { const order = await this.orderRepository.findOne(orderId); if (!order) { throw new NotFoundException(`Order with ID ${orderId} not found`); } if (order.status !== 'PENDING') { throw new BadRequestException('Order is not in a valid state'); } return order; } async confirmOrder(order: Order): Promise<Order> { order.status = 'CONFIRMED'; return this.orderRepository.save(order); }
}
En este ejemplo, la orden se valida tanto en términos de existencia como en el estado adecuado antes de proceder con cualquier operación, lo que evita errores y garantiza que solo se procesen datos válidos.
5. Conclusión
La combinación de la validación de sintaxis y semántica en NestJS resulta esencial para construir aplicaciones robustas y seguras. Validar los datos a través de DTOs, class-validator
, y servicios no solo mejora la calidad del código, sino que también asegura que los datos procesados sean correctos y estén alineados con las reglas de negocio.
El uso de pipes personalizados para la validación y transformación de datos añade un nivel adicional de flexibilidad y control, permitiendo crear soluciones a medida que cumplen con los requisitos específicos de la aplicación.
Al seguir estas prácticas, podemos crear APIs más confiables, reducir la posibilidad de errores y asegurar que nuestra aplicación maneje los datos de manera efectiva y segura.