Introducción:
En el mundo del desarrollo de aplicaciones web, la validación de datos es una de las tareas más críticas. Los datos que llegan a un servidor deben ser verificados para asegurarse de que cumplen con las expectativas de la aplicación y no comprometen su funcionamiento. En NestJS, un poderoso framework basado en Node.js, la validación se puede dividir en dos etapas clave: validación de sintaxis y validación semántica. En este artículo, exploraremos cómo NestJS permite manejar ambas etapas de manera efectiva, utilizando DTOs para la sintaxis y servicios para la semántica. Además, discutiremos cómo y cuándo transformar datos en los controladores para garantizar un flujo de datos limpio y coherente.
1. Validación de Sintaxis en NestJS: Un Enfoque a Través de DTOs
La validación de sintaxis se refiere a la verificación de que los datos recibidos en una petición HTTP tienen la estructura y el formato correcto. NestJS proporciona una manera eficiente de realizar esta validación utilizando DTOs (Data Transfer Objects) y la librería class-validator
. Esta etapa es crucial porque garantiza que los datos que ingresan al sistema están en el formato correcto antes de ser procesados más a fondo.
1.1 ¿Qué son los DTOs en NestJS?
Los DTOs son clases TypeScript que definen la forma esperada de los datos en las peticiones HTTP. Al definir un DTO, estás especificando explícitamente qué campos espera tu aplicación, qué tipos de datos son aceptables y qué reglas deben cumplirse. Estos DTOs actúan como una capa protectora entre el cliente y tu aplicación, asegurando que solo los datos que cumplen con las reglas establecidas lleguen a los controladores.
Ejemplo básico de DTO:
import { IsString, IsNotEmpty, IsEmail, IsOptional } from 'class-validator'; export class CreateUserDto { @IsNotEmpty() @IsString() firstName: string; @IsNotEmpty() @IsString() lastName: string; @IsNotEmpty() @IsEmail() email: string; @IsOptional() @IsString() phone?: string;
}
En este ejemplo, CreateUserDto
define un DTO para crear un nuevo usuario. Aquí, estamos utilizando varios decoradores de class-validator
:
-
@IsNotEmpty()
asegura que el campo no sea nulo o vacío. -
@IsString()
verifica que el campo sea una cadena de texto. -
@IsEmail()
valida que el campo sea un correo electrónico válido. -
@IsOptional()
indica que el campophone
es opcional.
1.2 Uso de class-validator
para validaciones complejas de sintaxis
La librería class-validator
proporciona una amplia gama de decoradores para realizar validaciones de sintaxis. Algunos de los más comunes incluyen:
-
@IsInt()
: Verifica que el valor sea un número entero.
@IsInt() age: number;
-
@IsBoolean()
: Verifica que el valor sea un booleano.
@IsBoolean() isActive: boolean;
-
@Length(min: number, max: number)
: Verifica que la longitud de la cadena esté dentro de un rango específico.
@Length(10, 20) username: string;
-
@IsArray()
: Verifica que el valor sea un array.
@IsArray() tags: string[];
-
@ArrayMinSize(size: number)
: Verifica que el array tenga un tamaño mínimo.
@ArrayMinSize(1) tags: string[];
Validación condicional y lógica más avanzada:
class-validator
también permite realizar validaciones más avanzadas utilizando decoradores como @ValidateIf()
y @Matches()
:
-
@ValidateIf(condition: (obj, value) => boolean)
: Este decorador permite aplicar una validación solo si se cumple una cierta condición.
@ValidateIf(o => o.isActive) @IsNotEmpty() activationDate: string;
En este caso, activationDate
solo será requerido si isActive
es true
.
-
@Matches(pattern: RegExp, message?: string)
: Verifica que el valor coincida con una expresión regular.
@Matches(/^[A-Z][a-z]*$/, { message: 'The lastName must start with an uppercase letter.', }) lastName: string;
1.3 Pipes personalizados: Añadiendo validaciones de sintaxis personalizadas
Mientras que class-validator
cubre la mayoría de las necesidades de validación de sintaxis, en algunos casos, es necesario aplicar validaciones más específicas o personalizadas. Aquí es donde los pipes personalizados entran en juego. Los pipes son clases que implementan la interfaz PipeTransform
y pueden ser utilizados para transformar o validar datos antes de que lleguen a un controlador.
Creando un pipe personalizado para validar un enum:
Imaginemos que tenemos un enum de roles y queremos asegurarnos de que un campo en particular solo acepte valores dentro de ese enum.
import { ArgumentMetadata, BadRequestException, PipeTransform } from '@nestjs/common'; enum UserRole { ADMIN = 'admin', USER = 'user', GUEST = 'guest',
} export class ParseEnumPipe implements PipeTransform { constructor(private readonly enumType: object) {} transform(value: any, metadata: ArgumentMetadata) { if (!Object.values(this.enumType).includes(value)) { throw new BadRequestException(`Invalid value: ${value}`); } return value; }
}
En este pipe, ParseEnumPipe
valida que el valor de UserRole
esté dentro de los valores permitidos en el enum. Si no es así, lanza una excepción de tipo BadRequestException
.
Utilizando el pipe personalizado en un controlador:
@Post()
createUser( @Body('role', new ParseEnumPipe(UserRole)) role: UserRole, @Body() createUserDto: CreateUserDto,
) { return this.userService.createUser(createUserDto);
}
Aquí estamos utilizando el ParseEnumPipe
para asegurarnos de que el campo role
solo reciba valores válidos según el enum UserRole
. Si el valor no es válido, el pipe lanzará una excepción antes de que la solicitud llegue al servicio.
2. Validación Semántica en NestJS: Más Allá de la Sintaxis
Mientras que la validación de sintaxis garantiza que los datos tienen la forma correcta, la validación semántica se centra en verificar que los datos tienen sentido en el contexto de la aplicación. Esto es crucial para mantener la integridad de la lógica de negocio y garantizar que las acciones realizadas por los usuarios estén permitidas según las reglas de la aplicación.
2.1 ¿Qué es la validación semántica?
La validación semántica se ocupa de la lógica del negocio. Verifica si los datos, aunque sean sintácticamente correctos, son semánticamente válidos. Por ejemplo, si se intenta realizar una transacción en una cuenta bancaria, la validación semántica se encargaría de verificar que la cuenta existe, que tiene suficientes fondos y que la transacción está permitida según las reglas del negocio.
Ejemplos de validación semántica:
- Verificación de existencia en la base de datos:
async validateUser(userId: string) { const user = await this.userRepository.findOne(userId); if (!user) { throw new NotFoundException(`User with ID ${userId} not found`); } return user; }
En este ejemplo, el método validateUser
en un servicio verifica si un usuario con un userId
específico existe en la base de datos. Si no existe, se lanza una excepción de tipo NotFoundException
.
- Validación de reglas de negocio:
async validateTransaction(amount: number, accountId: string) { const account = await this.accountRepository.findOne(accountId); if (account.balance < amount) { throw new BadRequestException('Insufficient funds'); } return true; }
Aquí, el método validateTransaction
se asegura de que una cuenta tenga suficientes fondos antes de permitir una transacción.
2.2 Implementando validación semántica en los servicios
La validación semántica generalmente se implementa en los servicios de la aplicación. Esto permite que la lógica de negocio esté encapsulada y separada de la lógica de enrutamiento que maneja el controlador. Esto es importante para mantener el código modular, fácil de mantener y testear.
Ejemplo de un servicio con validación semántica:
@Injectable()
export class OrderService { constructor(private readonly orderRepository: OrderRepository) {} 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; }
}
En este servicio, validateOrder
verifica si una orden con un orderId
específico existe y está en un estado válido (PENDING
). Si alguna de estas condiciones no se cumple, se lanzan excepciones apropiadas.
Utilizando la validación semántica en un controlador:
@Post(':orderId/confirm')
async confirmOrder( @Param('orderId') orderId: string,
) { const order = await this.orderService.validateOrder(orderId); return this.orderService.confirmOrder(order);
}
En este controlador, confirmOrder
utiliza el método validateOrder
para asegurarse de que la orden existe y está en un estado válido antes de proceder con la confirmación.
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
.
Conclusión
La validación de datos en NestJS es una práctica fundamental que garantiza la integridad y seguridad de una aplicación. A través de la validación de sintaxis con DTOs y class-validator
, y la validación semántica dentro de los servicios, NestJS permite construir aplicaciones robustas y resistentes a errores. Además, el uso estratégico de pipes para la transformación de datos asegura que las diferentes capas de la aplicación se mantengan coherentes y alineadas con las necesidades del negocio. Con estas herramientas, los desarrolladores pueden crear aplicaciones que no solo funcionan correctamente, sino que también son fáciles de mantener y escalar.
Discover more from Coursity
Subscribe to get the latest posts sent to your email.