Validación con Joi y Nestjs 🔒

Antonio Duprez
7 min readFeb 21, 2021

NestJS

NestJS es un framework progresivo para Node, que nos permite crear aplicaciones escalables y eficientes usando TypeScript. Por defecto, NestJS utiliza Express, pero también soporta Fastify.

NestJS está inspirado en Angular y tiene grandes similitudes con este framework, como por ejemplo los servicios, los interceptores, las pipes, etc

Gracias a NestJS podemos montar un backend en cuestión de minutos, ya que proporciona una estructura de directorios sólida y nos provee de una gran cantidad de utilidades.

Joi

Joi es la herramienta más poderosa para la validación de datos en JavaScript.
Este módulo permite crear esquemas de datos usando un lenguaje simple, comprensible e intuitivo.

A la hora de interactuar con una API, es muy importante que se validen los datos tanto en el lado del servidor como en el del cliente para tener controlados posibles errores y la forma en la que se interactúa con la base de datos.

Anteriormente Joi pertenecía al ecosistema de Hapi, un framework para crear aplicaciones en Node, pero a partir de la versión 12.1.0 decidió dejar de formar parte de este framework y ser un paquete totalmente independiente, lo cual facilita aún más la integración con este.

Integrar Joi en nuestro proyecto

Lo instalaremos como dependencia en el proyecto:

$ npm install --save joi
$ npm install --save-dev @types/joi

Para utilizar Joi hay que utilizar las pipes de NestJS. Una pipe transforma los datos de entrada en el formato de salida deseado y en esta caso validará los datos de la petición (headers, body, params, query params).

Todas las pipes deben de implementar la interfaz PipeTransform. Esta interfaz obliga a implementar un método transform que será donde se realice el formateo y la validación de los datos:

joi-validation.pipe.ts

Este método recibe los datos que se van a formatear y validar. Además, será ejecutado automáticamente después de definir la ruta en el controlador.

En el constructor se define un esquema, ya que a la hora de invocar esta pipe hay que hacer uso de los esquemas de Joi.

Configuración de Joi

A la hora de usar el método validate existen varios parámetros de configuración. Algunas son:

  • abortEarly: Detiene la validación del esquema en cuanto encuentra un error.
  • allowUnknown: Ignora las claves que no estén definidas en el esquema.
  • stripUnknown: Elimina las claves que no estén definidas en el esquema de los datos que validamos. Esta opción es de las más útiles que tiene.
  • noDefaults: No permite que haya valores por defecto definidos en los esquemas.
  • noEnumerables: No permite que haya enumerados definidos en los esquemas.
const { error, value } = this.schema.validate(initialValue, {
abortEarly: false,
allowUnknown: true,
stripUnknown: true
});

Tipos básicos de validaciones

Joi tiene una documentación muy clara y explicativa sobre cómo crear un esquema de datos. Existen muchos tipos de datos, pero los más comúnes son:

  • String: valida que el dato sea una cadena de texto. Por defecto la cadena de texto debe de estar rellena, es decir, '' no sería aceptado.
  • Number: valida que el dato sea de tipo numérico.
  • Any: acepta cualquier tipo de dato, pero debe de estar definido en los datos que se van a validar.
  • Object: comprueba que sea un objeto. Opcionalmente permite incluir una validación de las propiedades que tendrá dicho objeto.
  • Array: comprueba que sea un array. Opcionalmente permite incluir una validación para cada item del array.
users.schemas.ts

Joi permite tanto validar datos, como poder modificarlos. Por ejemplo, en este esquema se comprueba que firstName y lastName sean cadenas de texto obligatorias y además se le aplica el método trim() de JavaScript, eliminando así los espacios del principio y del final del valor de estas propiedades.

Implementación

Para realizar la validación de los datos hay que definir la llamada a la pipe después de definir la ruta del endpoint.

Existen dos decoradores de NestJS que nos permitirán validar los datos. En ambos casos hay que incluir una instancia de la pipe creada y del esquema que queremos validar:

  • Usando el decorador @UsePipes(): En este caso se van a validar todos los parámetros que tenga la función del controlador. Es decir, en este caso solo hay un @Body(), pero si hubiese un @Param() usaría el mismo esquema de validación.
users.controller.ts
  • Usando los decoradores @Body(), @Param(), @Query(): En este caso validará únicamente los datos del decorador que se use. Personalmente, recomiendo validar los datos siempre de esta manera, ya que siempre serán diferentes estructuras de datos y por tanto deben de ser esquemas unilaterales.
users.controller.ts

Manejo de errores

Si ejecuto una petición en Postman a esta API sin incluir alguno de los campos requeridos, por defecto devuelve este error:

Anteriormente en la pipe se ha usado la utilidad de BadRequestException de NestJS para devolver este tipo de error, pero sería idóneo devolver algo más comprensible para que del lado del cliente pueda ser interpretado:

Esta es una posible estructura para devolver los errores agrupados por cada campo. De esta manera el cliente puede interpretar que errores tiene cada campo. Cada campo puede tener varios errores, por eso es un array.

Para conseguir esta estructura lo primero que hay que hacer es crear los mensajes que se van a mostrar según cada tipo de error.

Para ello he creado un objeto que contiene las claves que hacen referencia a cada tipo de validación de Joi:

errors = {
any: {
required: 'Este campo es obligatorio',
},
string: {
min: 'La longitud mínima es {{limit}}',
max: 'La longitud máxima es {{limit}}',
},
number: {
min: 'El valor mínimo es {{limit}}',
max: 'El valor máximo es {{limit}}',
},
date: {
min: 'La fecha mínima es {{limit}}',
max: 'La fecha máxima es {{limit}}',
},
};

Este es un pequeño ejemplo, existen algunos repos que según el idioma te dan ya todas las traducciones de cada error. Lo ideal es usar i18n para las traducciones de los errores y cada clave debe corresponder a la validación usada en Joi, pero deberá ir anidada dentro de su correspondiente “tipo base”.

Necesitaremos implementar algunos métodos para procesar y agrupar estos errores:

  • getErrorLimit: Dependiendo del tipo de error que sea la variable “limit” puede ser formateada de distinta forma, por ejemplo, si es una fecha y queremos devolverla con formato UTC.
  • getErrorMessage: Es el encargar de encontrar el mensaje adecuado en el objeto donde están definidos todos los errores. Si la clave no estuviese definida asigna un mensaje por defecto.
  • assignErrorToField: Como expliqué anteriormente, una propiedad puede tener varios errores. Esta función es la encargada de asignar cada error al campo correcto.

Bonus: Como crear un decorador de Nestjs para integrarlo con la pipe

Un decorador no es más que una función encapsulada a la que se llama usando la nomenclatura de @NombreFuncion(params). En este caso, Nest permite crear un decorador e integrarlo como parte de su ecosistema y usarlo junto a sus otros decoradores de forma muy sencilla.

Para ello simplemente hay que definir una función y exportarla:

import { Body } from '@nestjs/common';
import { JoiValidationPipe } from '@shared/pipes/joi-validation.pipe';
import { AnySchema } from 'joi';
export function ValidatedBody(schema: AnySchema, propertyName?: string) {
if (propertyName) {
return Body(propertyName, new JoiValidationPipe(schema));
} else {
return Body(new JoiValidationPipe(schema));
}
}

Esta función evaluará si han pasado la propiedad propertyName y en ese caso hará uno del decorador Body de Nest para seleccionar un campo concreto del body y aplicarle un schema haciendo uso de la pipe configurada anteriormente. En caso contrario, aplicará la validación a todo el body entero.

Para invocarlo simplemente deberemos de hacerlo así:

@ValidatedBody(joiSchema) body: any

o

@ValidatedBody(joiSchema, propertyName) propertyOfBody: any

De esta forma queda un poco más organizado el tener que usar la pipe “JoiValidationPipe” y no habrá que impotarla en cada archivo.

Conclusión

Como hemos visto Joi no solo facilita el tener una capa de seguridad a la hora de crear nuestra API, sino que también permite modificar los datos que provienen del cliente y ahorrarnos hacer operaciones “manuales” en los controladores.

Existen muchas herramientas para la validación de datos, pero sin duda por mi experiencia recomiendo usar Joi ya que es la más completa y además aprender a usarlo y dominarlo es muy sencillo.

Además también permite crear tus propios validadores con la función Joi.extend() lo cual hace que el código sea mucho mas reutilizable y organizado.

Espero que os haya gustado el artículo.
¡Muchas gracias por leerlo!

--

--

Antonio Duprez

Backend Developer #nodejs #redis #nestjs #typeorm #mongodb #angular #vuejs #ionic https://portfolio-antonio-duprez.web.app/