Cómo utilizar Joi para la validación del esquema de API de Node

Introducción
Imagina que estás trabajando en un punto final de API para crear un nuevo usuario. Los datos del usuario (como firstname
, lastname
, age
y birthdate
) deberán incluirse en la solicitud. age
No sería deseable que un usuario ingrese por error su nombre como valor del campo cuando espera un valor numérico. Tampoco sería deseable que un usuario ingrese su fecha de nacimiento en el birthdate
campo cuando espera un formato de fecha particular. No quieres que los datos incorrectos ingresen en tu aplicación. Puedes solucionar esto con la Validación de datos.
Si alguna vez ha utilizado un ORM (mapeo relacional de objetos) al crear su aplicación Node (como Sequelize, Knex, Mongoose [para MongoDB]), sabrá que es posible establecer restricciones de validación para los esquemas de su modelo. Esto facilita el manejo y la validación de datos en el nivel de la aplicación antes de guardarlos en la base de datos. Al crear API, los datos generalmente provienen de solicitudes HTTP a ciertos puntos finales, y pronto puede surgir la necesidad de poder validar los datos en el nivel de solicitud.
En este tutorial, aprenderá cómo podemos usar el módulo de validación de Joi para validar datos en el nivel de solicitud. Puede obtener más información sobre cómo usar Joi y los tipos de esquemas admitidos consultando la Referencia de API.
Al finalizar este tutorial, deberías poder hacer lo siguiente:
- Crear un esquema de validación para los parámetros de datos de la solicitud
- Manejar errores de validación y brindar retroalimentación adecuada
- Crear un middleware para interceptar y validar solicitudes
Prerrequisitos
Para completar este tutorial, necesitarás:
- Un entorno de desarrollo local para Node.js. Siga el tutorial Cómo instalar Node.js y crear un entorno de desarrollo local.
- Se recomienda descargar e instalar una herramienta como Postman para probar los puntos finales de API.
Este tutorial fue verificado con Node v14.2.0, npm
v6.14.5 y joi
v13.0.2.
Paso 1: Configuración del proyecto
Para este tutorial, simularás que estás creando un portal escolar y deseas crear puntos finales de API:
/people
:Agregar nuevos estudiantes y profesores/auth/edit
:Establecer credenciales de inicio de sesión para profesores/fees/pay
:realizar pagos de cuotas para estudiantes
Creará una API REST para este tutorial usando Express para probar sus esquemas Joi.
Para comenzar, abra su terminal de línea de comandos y cree un nuevo directorio de proyecto:
- mkdir joi-schema-validation
Luego navega hasta ese directorio:
- cd joi-schema-validation
Ejecute el siguiente comando para configurar un nuevo proyecto:
- npm init -y
E instala las dependencias necesarias:
- npm install body-parser@1.18.26 express@4.16.2 joi@13.0.2 lodash@4.17.4 morgan@1.9.0^
Cree un nuevo archivo con el nombre app.js
en el directorio raíz de su proyecto para configurar la aplicación Express:
- nano app.js
A continuación se muestra una configuración inicial para la aplicación.
Primero, se requiere express
, morgan
y body-parser
:
aplicación.js
// load app dependenciesconst express = require('express');const logger = require('morgan');const bodyParser = require('body-parser');
Luego, inicialice el app
:
aplicación.js
// ...const app = express();const port = process.env.NODE_ENV || 3000;// app configurationsapp.set('port', port);// establish http server connectionapp.listen(port, () = { console.log(`App running on port ${port}`) });
A continuación, agregue morgan
el registro y body-parser
los middlewares al flujo de solicitudes de su aplicación:
aplicación.js
// ...const app = express();const port = process.env.NODE_ENV || 3000;// app configurationsapp.set('port', port);// load app middlewaresapp.use(logger('dev'));app.use(bodyParser.json());app.use(bodyParser.urlencoded({ extended: false }));// establish http server connectionapp.listen(port, () = { console.log(`App running on port ${port}`) });
Estos middlewares obtienen y analizan el cuerpo de la solicitud HTTP actual para application/json
las application/x-www-form-urlencoded
solicitudes y los ponen a disposición en el req.body
middleware de manejo de ruta de la solicitud.
Luego añade Routes
:
aplicación.js
// ...const Routes = require('./routes');const app = express();const port = process.env.NODE_ENV || 3000;// app configurationsapp.set('port', port);// load app middlewaresapp.use(logger('dev'));app.use(bodyParser.json());app.use(bodyParser.urlencoded({ extended: false }));// load our API routesapp.use('/', Routes);// establish http server connectionapp.listen(port, () = { console.log(`App running on port ${port}`) });
Su app.js
archivo está completo por el momento.
Manejo de los puntos finales
Desde la configuración de su aplicación, especificó que está obteniendo sus rutas de un routes.js
archivo.
Creemos el archivo en el directorio raíz de su proyecto:
- nano routes.js
Requerir express
y gestionar solicitudes con una respuesta de "success"
y los datos en la solicitud:
rutas.js
const express = require('express');const router = express.Router();// generic route handlerconst genericHandler = (req, res, next) = { res.json({ status: 'success', data: req.body });};module.exports = router;
A continuación, establezca puntos finales para people
, auth/edit
y fees/pay
:
rutas.js
// ...// create a new teacher or studentrouter.post('/people', genericHandler);// change auth credentials for teachersrouter.post('/auth/edit', genericHandler);// accept fee payments for studentsrouter.post('/fees/pay', genericHandler);module.exports = router;
Ahora, cuando una solicitud POST llega a cualquiera de estos puntos finales, su aplicación utilizará el genericHandler
y enviará una respuesta.
Por último, agrega un start
script a la scripts
sección de tu package.json
archivo:
- nano package.json
Debería verse así:
paquete.json
// ..."scripts": { "start": "node app.js"},// ...
Ejecuta la aplicación para ver lo que tienes hasta ahora y que todo esté funcionando correctamente:
- npm start
Deberías ver un mensaje como el siguiente: "App running on port 3000"
. Anota el número de puerto en el que se está ejecutando el servicio y deja la aplicación ejecutándose en segundo plano.
Prueba de los puntos finales
Puede probar los puntos finales de la API utilizando una aplicación como Postman.
Nota: Si es la primera vez que utiliza Postman, aquí le mostramos algunos pasos sobre cómo usarlo para este tutorial:
- Comience creando una nueva solicitud.
- Establezca su tipo de solicitud en POST (de manera predeterminada, puede configurarse en GET ).
- Complete el campo Ingresar URL de solicitud con la ubicación del servidor (en la mayoría de los casos, debería ser:
localhost:3000
) y el punto final (en este caso:/people
). - Seleccionar cuerpo .
- Establezca su tipo de codificación en Raw (de manera predeterminada, puede estar configurado en ninguno “).
- Establezca el formato en JSON (de forma predeterminada, puede estar establecido en Texto ).
- Introduzca sus datos.
Luego haga clic en Enviar para ver la respuesta.
Consideremos un escenario en el que un administrador está creando una nueva cuenta para un profesor llamado "Glad Chinda".
Proporcione este ejemplo de solicitud:
{ "type": "TEACHER", "firstname": "Glad", "lastname": "Chinda"}
Recibirás esta respuesta de ejemplo:
Output{ "status": "success", "data": { "type": "TEACHER", "firstname": "Glad", "lastname": "Chinda" }}
Habrás recibido un "success"
estado y los datos que enviaste se capturarán en la respuesta. Esto verifica que tu aplicación está funcionando como se esperaba.
Paso 2: Experimentar con las reglas de validación de Joi
Un ejemplo simplificado puede ayudarle a tener una idea de lo que logrará en los pasos posteriores.
En este ejemplo, creará reglas de validación con Joi para validar un correo electrónico, un número de teléfono y una fecha de nacimiento para una solicitud de creación de un nuevo usuario. Si la validación falla, devolverá un error. De lo contrario, devolverá los datos del usuario.
Agreguemos un test
punto final al app.js
archivo:
- nano app.js
Agregue el siguiente fragmento de código:
aplicación.js
// ...app.use('/', Routes);app.post('/test', (req, res, next) = { const Joi = require('joi'); const data = req.body; const schema = Joi.object().keys({ email: Joi.string().email().required(), phone: Joi.string().regex(/^d{3}-d{3}-d{4}$/).required(), birthday: Joi.date().max('1-1-2004').iso() });});// establish http server connectionapp.listen(port, () = { console.log(`App running on port ${port}`) });
Este código agrega un nuevo /test
punto final. Lo define data
desde el cuerpo de la solicitud y lo define schema
con reglas Joi para email
, phone
, y birthday
.
Las restricciones email
incluyen:
- Debe ser una cadena de correo electrónico válida
- Debe ser requerido
Las restricciones phone
incluyen:
- Debe ser una cadena con dígitos en el formato de
XXX-XXX-XXXX
- Debe ser requerido
Las restricciones birthday
incluyen:
- Debe ser una fecha válida en formato ISO 8601
- No puede ser posterior al 1 de enero de 2004.
- No es necesario
A continuación, gestione la validación aprobada y fallida:
aplicación.js
// ...app.use('/', Routes);app.post('/test', (req, res, next) = { // ... Joi.validate(data, schema, (err, value) = { const id = Math.ceil(Math.random() * 9999999); if (err) { res.status(422).json({ status: 'error', message: 'Invalid request data', data: data }); } else { res.json({ status: 'success', message: 'User created successfully', data: Object.assign({id}, value) }); } });});// establish http server connectionapp.listen(port, () = { console.log(`App running on port ${port}`) });
Este código toma el data
y lo valida contra el schema
.
Si alguna de las reglas para email
, phone
, o birthday
falla, se genera un error 422 con un estado de "error"
y un mensaje de "Invalid request data"
.
Si se cumplen todas las reglas para email
, phone
, y birthday
, se genera una respuesta con un estado de "success"
y un mensaje de "User created successfully"
.
Ahora puedes probar la ruta de ejemplo.
Inicie la aplicación nuevamente ejecutando el siguiente comando desde su terminal:
- npm start
Puede utilizar Postman para probar la ruta de ejemplo POST /test
.
Configura tu solicitud:
POST localhost:3000/testBodyRawJSON
Añade tus datos al campo JSON:
{ "email": "test@example.com", "phone": "555-555-5555", "birthday": "2004-01-01"}
Debería ver algo similar a la siguiente respuesta:
Output{ "status": "success", "message": "User created successfully", "data": { "id": 1234567, "email": "test@example.com", "phone": "555-555-5555", "birthday": "2004-01-01T00:00:00.000Z" }}
A continuación se muestra un vídeo de demostración que permite lograr este objetivo:
Puede especificar más restricciones de validación en el esquema base para controlar el tipo de valores que se consideran válidos. Dado que cada restricción devuelve una instancia de esquema, es posible encadenar varias restricciones mediante el encadenamiento de métodos para definir reglas de validación más específicas.
Se recomienda crear esquemas de objetos utilizando Joi.object()
o Joi.object().keys()
. Al utilizar cualquiera de estos dos métodos, puede controlar aún más las claves permitidas en el objeto utilizando algunas restricciones adicionales, lo que no será posible con el método literal de objeto.
A veces, es posible que desee que un valor sea una cadena, un número o algo más. Aquí es donde entran en juego los esquemas alternativos. Puede definir esquemas alternativos utilizando Joi.alternatives()
. Hereda del esquema, por lo que se pueden usar any()
restricciones como con él.required()
Consulte la Referencia de API para obtener documentación detallada de todas las restricciones disponibles.
Paso 3: Creación de los esquemas de API
Después de familiarizarse con las restricciones y los esquemas en Joi, ahora puede crear los esquemas de validación para las rutas API.
Cree un nuevo archivo llamado schemas.js
en el directorio de ruta del proyecto:
- nano schemas.js
Comience por solicitar Joi:
esquemas.js
// load Joi moduleconst Joi = require('joi');
peoplePunto final ypersonDataSchema
El /people
punto final utilizará personDataSchema
. En este escenario, un administrador está creando cuentas para profesores y estudiantes. La API necesitará un id
, type
, name
y posiblemente un age
si son estudiantes.
id
: será una cadena en formato UUID v4:
Joi.string().guid({version: 'uuidv4'})
type
: será una cadena de STUDENT
o TEACHER
. La validación aceptará cualquier caso, pero forzará uppercase()
:
Joi.string().valid('STUDENT', 'TEACHER').uppercase().required()
age
: será un número entero o una cadena con un valor mayor que 6
. La cadena también puede contener formatos abreviados de “año” (como “y”, “yr” y “yrs”):
Joi.alternatives().try([ Joi.number().integer().greater(6).required(), Joi.string().replace(/^([7-9]|[1-9]d+)(y|yr|yrs)?$/i, '$1').required()]);
firstname
, lastname
, fullname
: será una cadena de caracteres alfabéticos. La validación aceptará mayúsculas y minúsculas, pero forzará uppercase()
:
Una cadena de caracteres alfabéticos para firstname
y lastname
:
Joi.string().regex(/^[A-Z]+$/).uppercase()
Un espacio separado fullname
:
Joi.string().regex(/^[A-Z]+ [A-Z]+$/i).uppercase()
Si fullname
se especifica, entonces se deben omitir y. Si firstname
se especifica, entonces también se debe especificar. Se debe especificar uno de los dos: olastname
firstname
lastname
fullname
firstname
.xor('firstname', 'fullname').and('firstname', 'lastname').without('fullname', ['firstname', 'lastname'])
Poniéndolo todo junto, peopleDataSchema
se parecerá a esto:
esquemas.js
// ...const personID = Joi.string().guid({version: 'uuidv4'});const name = Joi.string().regex(/^[A-Z]+$/).uppercase();const ageSchema = Joi.alternatives().try([ Joi.number().integer().greater(6).required(), Joi.string().replace(/^([7-9]|[1-9]d+)(y|yr|yrs)?$/i, '$1').required()]);const personDataSchema = Joi.object().keys({ id: personID.required(), firstname: name, lastname: name, fullname: Joi.string().regex(/^[A-Z]+ [A-Z]+$/i).uppercase(), type: Joi.string().valid('STUDENT', 'TEACHER').uppercase().required(), age: Joi.when('type', { is: 'STUDENT', then: ageSchema.required(), otherwise: ageSchema })}).xor('firstname', 'fullname').and('firstname', 'lastname').without('fullname', ['firstname', 'lastname']);
/auth/editPunto final yauthDataSchema
El /auth/edit
punto final utilizará authDataSchema
. En este escenario, un profesor está actualizando el correo electrónico y la contraseña de su cuenta. La API necesitará id
, email
, password
y confirmPassword
.
id
:utilizará la validación definida anteriormente para personDataSchema
.
email
: será una dirección de correo electrónico válida. La validación aceptará cualquier mayúscula y minúscula, pero forzará el uso de lowercase()
.
Joi.string().email().lowercase().required()
password
: será una cadena de al menos 7
caracteres:
Joi.string().min(7).required().strict()
confirmPassword
: será una cadena que hará referencia password
para garantizar que ambos coincidan:
Joi.string().valid(Joi.ref('password')).required().strict()
Poniéndolo todo junto, authDataSchema
se parecerá a esto:
esquemas.js
// ...const authDataSchema = Joi.object({ teacherId: personID.required(), email: Joi.string().email().lowercase().required(), password: Joi.string().min(7).required().strict(), confirmPassword: Joi.string().valid(Joi.ref('password')).required().strict()});
/fees/payPunto final yfeesDataSchema
El /fees/pay
punto final utilizará feesDataSchema
. En este escenario, un estudiante envía la información de su tarjeta de crédito para pagar una cantidad de dinero y también se registra la marca de tiempo de la transacción. La API necesitará id
, amount
, cardNumber
y completedAt
.
id
:utilizará la validación definida anteriormente para personDataSchema
.
amount
: será un número entero o un número de punto flotante. El valor debe ser un número positivo mayor que 1
. Si se proporciona un número de punto flotante, la precisión se trunca a un máximo de 2
:
Joi.number().positive().greater(1).precision(2).required()
cardNumber
: será una cadena que es un número válido compatible con el algoritmo de Luhn:
Joi.string().creditCard().required()
completedAt
: será una marca de tiempo y fecha en formato JavaScript:
Joi.date().timestamp().required()
Poniéndolo todo junto, feesDataSchema
se parecerá a esto:
esquemas.js
// ...const feesDataSchema = Joi.object({ studentId: personID.required(), amount: Joi.number().positive().greater(1).precision(2).required(), cardNumber: Joi.string().creditCard().required(), completedAt: Joi.date().timestamp().required()});
Por último, exporte un objeto con los puntos finales asociados a los esquemas:
esquemas.js
// ...// export the schemasmodule.exports = { '/people': personDataSchema, '/auth/edit': authDataSchema, '/fees/pay': feesDataSchema};
Ahora, ha creado esquemas para los puntos finales de la API y los ha exportado en un objeto con los puntos finales como claves.
Paso 4: Creación del middleware de validación del esquema
Creemos un middleware que interceptará cada solicitud a los puntos finales de su API y validará los datos de la solicitud antes de entregar el control al controlador de ruta.
Cree una nueva carpeta con el nombre middlewares
en el directorio raíz del proyecto:
- mkdir middlewares
Luego crea un nuevo archivo SchemaValidator.js
dentro del cual se le aplicará el nombre:
- nano middlewares/SchemaValidator.js
El archivo debe contener el siguiente código para el middleware de validación del esquema.
middlewares/SchemaValidator.js
const _ = require('lodash');const Joi = require('joi');const Schemas = require('../schemas');module.exports = (useJoiError = false) = { // useJoiError determines if we should respond with the base Joi error // boolean: defaults to false const _useJoiError = _.isBoolean(useJoiError) useJoiError; // enabled HTTP methods for request data validation const _supportedMethods = ['post', 'put']; // Joi validation options const _validationOptions = { abortEarly: false, // abort after the last validation error allowUnknown: true, // allow unknown keys that will be ignored stripUnknown: true // remove unknown keys from the validated data }; // return the validation middleware return (req, res, next) = { const route = req.route.path; const method = req.method.toLowerCase(); if (_.includes(_supportedMethods, method) _.has(Schemas, route)) { // get schema for the current route const _schema = _.get(Schemas, route); if (_schema) { // Validate req.body using the schema and validation options return Joi.validate(req.body, _schema, _validationOptions, (err, data) = { if (err) { // Joi Error const JoiError = { status: 'failed', error: { original: err._object, // fetch only message and type from each error details: _.map(err.details, ({message, type}) = ({ message: message.replace(/['"]/g, ''), type })) } }; // Custom Error const CustomError = { status: 'failed', error: 'Invalid request data. Please review request and try again.' }; // Send back the JSON error response res.status(422).json(_useJoiError ? JoiError : CustomError); } else { // Replace req.body with the data after Joi validation req.body = data; next(); } }); } } next(); };};
Aquí, has cargado Lodash junto con Joi y los esquemas en el módulo de middleware. También estás exportando una función de fábrica que acepta un argumento y devuelve el middleware de validación de esquemas.
El argumento de la función de fábrica es un boolean
valor que, cuando es true
, indica que se deben utilizar los errores de validación de Joi; de lo contrario, se utiliza un error genérico personalizado para los errores en el middleware. El valor predeterminado es false
si no se especifica o se proporciona un valor no booleano.
También ha definido el middleware para que solo maneje solicitudes POST
y PUT
solicitudes. El middleware omitirá todos los demás métodos de solicitud. También puede configurarlo, si lo desea, para agregar otros métodos que DELETE
puedan tomar un cuerpo de solicitud.
El middleware utiliza el esquema que coincide con la clave de ruta actual del Schemas
objeto que definimos anteriormente para validar los datos de la solicitud. La validación se realiza mediante el Joi.validate()
método con la siguiente firma:
data
:los datos a validar que en nuestro caso sonreq.body
.schema
:el esquema con el que validar los datos.options
: unobject
que especifica las opciones de validación. Estas son las opciones de validación que usamos:callback
: una devolución de llamadafunction
que se llamará después de la validación. Toma dos argumentos. El primero es elValidationError
objeto Joi si hubo errores de validación onull
si no hubo errores. El segundo argumento son los datos de salida.
Finalmente, en la función de devolución de llamada, Joi.validate()
devuelve el error formateado como una respuesta JSON con el 422
código de estado HTTP si hay errores, o simplemente sobrescribe req.body
con los datos de salida de validación y luego pasa el control al siguiente middleware.
Ahora puedes usar el middleware en tus rutas:
- nano routes.js
Modifique el routes.js
archivo de la siguiente manera:
rutas.js
const express = require('express');const router = express.Router();const SchemaValidator = require('./middlewares/SchemaValidator');const validateRequest = SchemaValidator(true);// generic route handlerconst genericHandler = (req, res, next) = { res.json({ status: 'success', data: req.body });};// create a new teacher or studentrouter.post('/people', validateRequest, genericHandler);// change auth credentials for teachersrouter.post('/auth/edit', validateRequest, genericHandler);// accept fee payments for studentsrouter.post('/fees/pay', validateRequest, genericHandler);module.exports = router;
Ejecutemos su aplicación para probarla:
- npm start
Estos son datos de prueba de muestra que puede utilizar para probar los puntos finales. Puede editarlos como desee.
Nota: Para generar cadenas UUID v4, puede utilizar el módulo UUID de Node o un generador de UUID en línea.
/peoplePunto final
En este escenario, un administrador está ingresando en el sistema a un nuevo estudiante llamado John Doe con una edad de 12 años:
{ "id": "a967f52a-6aa5-401d-b760-35eef7c68b32", "type": "Student", "firstname": "John", "lastname": "Doe", "age": "12yrs"}
Ejemplo de POST /people
respuesta de éxito:
Output{ "status": "success", "data": { "id": "a967f52a-6aa5-401d-b760-35eef7c68b32", "type": "STUDENT", "firstname": "JOHN", "lastname": "DOE", "age": "12" }}
En este escenario fallido, el administrador no ha proporcionado un valor para el age
campo requerido:
Output{ "status": "failed", "error": { "original": { "id": "a967f52a-6aa5-401d-b760-35eef7c68b32", "type": "Student", "fullname": "John Doe", }, "details": [ { "message": "age is required", "type": "any.required" } ] }}
/auth/editPunto final
En este escenario, un profesor está actualizando su correo electrónico y contraseña:
{ "teacherId": "e3464323-22c1-4e31-9ac5-9bde207d61d2", "email": "teacher@example.com", "password": "password", "confirmPassword": "password"}
Ejemplo de POST /auth/edit
respuesta de éxito:
Output{ "status": "success", "data": { "teacherId": "e3464323-22c1-4e31-9ac5-9bde207d61d2", "email": "teacher@example.com", "password": "password", "confirmPassword": "password" }}
En este escenario fallido, el profesor proporcionó una dirección de correo electrónico no válida y una contraseña de confirmación incorrecta:
Output{ "status": "failed", "error": { "original": { "teacherId": "e3464323-22c1-4e31-9ac5-9bde207d61d2", "email": "email_address", "password": "password", "confirmPassword": "Password" }, "details": [ { "message": "email must be a valid email", "type": "string.email" }, { "message": "confirmPassword must be of [ref:password]", "type": "any.allowOnly" } ] }}
/fees/payPunto final
En este escenario, un estudiante paga una tarifa con una tarjeta de crédito y registra una marca de tiempo para la transacción:
Nota: Para fines de prueba, utilice 4242424242424242
un número de tarjeta de crédito válido. Este número ha sido designado para fines de prueba por servicios como Stripe.
{ "studentId": "c77b8a6e-9d26-428a-9df1-e852473f886f", "amount": 134.9875, "cardNumber": "4242424242424242", "completedAt": 1512064288409}
Ejemplo de POST /fees/pay
respuesta de éxito:
Output{ "status": "success", "data": { "studentId": "c77b8a6e-9d26-428a-9df1-e852473f886f", "amount": 134.99, "cardNumber": "4242424242424242", "completedAt": "2017-11-30T17:51:28.409Z" }}
En este escenario fallido, el estudiante proporcionó un número de tarjeta de crédito no válido:
Output{ "status": "failed", "error": { "original": { "studentId": "c77b8a6e-9d26-428a-9df1-e852473f886f", "amount": 134.9875, "cardNumber": "5678901234567890", "completedAt": 1512064288409 }, "details": [ { "message": "cardNumber must be a credit card", "type": "string.creditCard" } ] }}
Puede completar la prueba de su aplicación con diferentes valores para observar la validación exitosa y fallida.
Conclusión
En este tutorial, creó esquemas para validar una colección de datos utilizando Joi y manejó la validación de datos de solicitud utilizando un middleware de validación de esquema personalizado en su canalización de solicitud HTTP.
Tener datos consistentes garantiza que se comportarán de manera confiable y esperada cuando haga referencia a ellos en su aplicación.
Para obtener un ejemplo de código completo de este tutorial, consulte el joi-schema-validation-sourcecode
repositorio en GitHub.
Deja una respuesta