Veremos diversas configuraciones más o menos avanzadas sobre las validaciones en Nest, como la localización de los mensajes de validación, la aplicación de diversas reglas al mismo tiempo, la transformación de los datos y otras cosas importantes.
En los últimos artículos hemos explicado numerosos aspectos de las validaciones en Nest. En este artículo vamos a terminar esta serie revisando algunas configuraciones más avanzadas de validaciones que podemos aplicar en las aplicaciones, que complementarán los conocimientos vistos hasta el momento en el Manual de Nest.
Veremos configuraciones de todo tipo, desde la posibilidad de localizar los mensajes de validación, evitar posibles errores con pipes asíncronos o configurar detalladamente cómo se aplicarán las distintas reglas de validación configuradas sobre un mismo campo, entre otras cosas.
Configurar un ValidationPipe por ruta
Si por cualquier razón deseas evitar generar pipes globales, como aprendimos a hacer con ValidationPipe a nivel de aplicación, también podemos crear métodos que especifiquen que se debe de usar ValidationPipe, por medio del decorador @UsePipes().
@Post()
@UsePipes(new ValidationPipe())
post(@Body() body: TagDto): Tag {
return this.tagsService.insert(body);
}
@UsePipes(new ValidationPipe())
tendría el efecto de producir la validación en este método post() en particular.
Los pipes pueden devolver promesas
Los pipes pueden devolver promesas, y nos referimos no solamente a las validaciones, también pipes de transformaciones. Por ejemplo puede que necesitemos validar que un id recibido exista en una tabla de la base de datos.
Este hecho, devolver promesas, implica que los métodos de los controladores los tenemos que marcar como async. Este tema ya lo habíamos comentado por encima anteriormente, pero vamos a ver ahora que los tipados de los valores de retorno de los métodos los vamos a tener que cambiar.
El tipado de los métodos que usan pipes es algo que tiene que ver más con TypeScript que con el propio framework, pero que te interesará para hacer un desarrollo adecuado en Nest.
En el código del método post() anterior indicamos que este método devolvería un elemento Tag, que es justamente lo que hace. Pero claro, al usar @usePipes()
estamos asumiendo que vamos a trabajar con un pipe, por lo que el método podría devolver una promesa y lo tendríamos que firmar como async.
¿Pero qué pasa si lo firmo como async? entonces no va a devolver el objeto requerido en el controlador, sino una promesa, entonces el valor de devolución nos lo marcará TypesScript como error:
Para solucionarlo vamos a indicar que nos devuelve una promesa y que una vez que se resuelva la promesa, tendremos un Tag.
@Post()
@UsePipes(new ValidationPipe())
async post(@Body() body: TagDto): Promise<Tag> {
return this.tagsService.insert(body);
}
El propio framework se encarga de esperar a resolver la promesa una vez nos llega y, cuando se acabe de resolver, devolver el Tag como salida. Gracias a esto, como práctica, cada método que usa pipe, lo podemos firmar como async, devuelva o no una promesa, simplemente para asegurarnos que sea o no asíncrono el pipe, nos siga funcionando la aplicación.
Devuelva la promesa o directamente un valor, el framework lidiará con ello, por lo que podemos usar async sin más preocupación que marcar correctamente el valor de retorno del método.
Cambiar los mensajes de error de las validaciones
Como hemos visto en anteriores ejemplos, Nest envía a nuestros clientes del API mensajes según encuentra errores de validación. Estos mensajes son bastante útiles y resultan suficientemente genéricos para que sean útiles para la mayoría de los proyectos, incluso pensando que un API en muchas ocasiones es normal que devuelva mensajes en inglés. Sin embargo, muchos desarrolladores necesitan realizar la localización de los mensajes de las validaciones
Entonces ¿cómo podríamos personalizar o traducir estos mensajes de validación? Se puede conseguir muy fácilmente por medio de un objeto de configuración que podemos indicar en el decorador de las reglas de validación que nos aporta class-validator
.
@IsString({
message: "El campo nombre debe de ser un String"
})
name: string;
Como ves, el decorador acepta un objeto como parámetro. Ese objeto tiene una propiedad opcional llamada "message" que permite configurar el mensaje de salida en caso de error de validación.
Ahora nuestro mensaje de error cuando la propiedad "name" no sea una cadena aparecerá localizada en perfecto español.
Validaciones con varias reglas
Otro tema interesante que tenemos pendiente de mostrar es cómo realizar unas validaciones un poco más detalladas. En ejemplos anteriores hemos visto que cada propiedad tenía una validación, pero esto no tiene que ser siempre así.
En el DTO podemos definir diversas reglas de validación para cada propiedad. Por ejemplo como tenemos aquí.
@IsInt({ message: 'El stock debe de ser un número entero' })
@Min(0, { message: 'El stock debe ser 0 o más'})
stock: number;
Fíjate que estamos indicando dos reglas. La primera indica que es un número entero y la segunda que el mínimo sería 0, algo bastante lógico porque el stock de un producto no debería ser negativo.
De manera predeterminada, si ninguna de las dos validaciones funciona, se generarán dos mensajes de errores de validación.
{
"statusCode": 400,
"message": [
"El stock debe ser 0 o más",
"El stock debe de ser un número entero"
],
"error": "Bad Request"
}
Si no te gusta que se generen más de un error de validación por cada propiedad, lo puedes configurar mediante las opciones de personalización de ValidationPipe, que vamos a ver a continuación.
Configuraciones útiles de ValidationPipe
Al crear el objeto ValidationPipe podemos enviarle una configuración al constructor, que nos sirve para personalizar el método de funcionamiento de las validaciones.
La configuración la enviamos como un objeto, tanto para pipes globales como para pipes configurados en una ruta en particular. De la siguiente manera:
app.useGlobalPipes(new ValidationPipe({
stopAtFirstError: true,
whitelist: true,
errorHttpStatusCode: 406,
}));
En la documentación de validación de Nest podemos encontrar el listado completo de las configuraciones posibles para las validaciones. Algunas que consideramos muy útiles serían:
- stopAtFirstError: permite solo un error por propiedad del DTO. Si falla una regla de validación de una propiedad del DTO deja de ejecutar las siguientes para esa propiedad. Esto no quiere decir que se pare con el primer error de cualquier propiedad, el método puede seguir enviando varios errores igualmente, porque podría enviar un error para cada propiedad.
- whitelist: permite realizar una limpieza de todas las propiedades que no están definidas en el DTO, para que el objeto enviado al controlador como body solo tenga las propiedades que se han definido en el DTO.
- forbidNonWhitelisted: esta propiedad si se indica a true entonces permite que la validación falla cuando se indica una propiedad en el body que no se esperaba en el DTO.
- skipMissingProperties: si es true, entonces la validación no fallará por las propiedades que estén faltando en el objeto del body.
- disableErrorMessages: esta propiedad la puedes poner a true si no deseas enviar los errores de validación a los clientes que usan el API.
- errorHttpStatusCode: aquí podemos informar el número del HTTP Status Code que se enviará cuando se produzca un error de validación
- transform: Si es true, permite transformar los objetos JSON planos a objetos de la clase del DTO.
Por ejemplo, si queremos indicar un error 406 y no informar de los mensajes de validación, podríamos escribir esta configuración.
app.useGlobalPipes(new ValidationPipe({
disableErrorMessages: true,
errorHttpStatusCode: 406,
}));
Entonces si ocurre cualquier cosa que no se valida correctamente, recibiríamos un escueto mensaje como este:
{
"statusCode": 406,
"message": "Not Acceptable"
}
Otro ejemplo interesante, al ponerle "transform" a true en un ValidationPipe global.
app.useGlobalPipes(new ValidationPipe({
transform: true,
}));
Ocurrirá que en las rutas se tratará de transformar los tipos de aquellos parámetros u objetos JSON recibidos en el body.
En este método, que recibimos un parámetro numérico en la URL, la variable id se transformará en number.
@Get(':id')
async find(@Param('id') id: number) {
console.log(id, typeof id);
return this.productsService.getId(id);
}
En este otro método post() el body dentro del método será de tipo TagDto, en vez de un simple objeto JSON genérico sin tipo.
@Post()
async post(@Body() body: TagDto): Promise<Tag> {
console.log(body);
return this.tagsService.insert(body);
}
Conclusión a las validaciones en Nest Framework
El tema de las validaciones es esencial en las aplicaciones y Nest lo tiene muy trabajado. Validar los objetos DTO es sencillo y existen diversas aproximaciones que hemos podido aprender mediante el uso de pipes.
Ahora es cuestión de practicar bastante y encontrar más detalles en la documentación del framework. En el siguiente artículo cambiaremos ya de tema y comenzaremos con algo que seguramente estarás esperando que es el acceso a las bases de datos con TypeORM.
Miguel Angel Alvarez
Fundador de DesarrolloWeb.com y la plataforma de formación online EscuelaIT. Com...