Validación de formulario inteligente Node.js

Publicado: 2022-03-11

Una de las tareas fundamentales a realizar en una API es la validación de datos. En este artículo, me gustaría mostrarle cómo agregar una validación a prueba de balas para sus datos de una manera que también los devuelva bien formateados.

Hacer una validación de datos personalizada en Node.js no es fácil ni rápido. Hay una gran cantidad de funciones que necesita escribir para cubrir cualquier tipo de datos. Si bien probé algunas bibliotecas de datos de formularios de Node.js, tanto para Express como para Koa, nunca cumplieron con las necesidades de mis proyectos. Hubo problemas con la extensión de las bibliotecas y las bibliotecas no funcionaban con estructuras de datos complejas o validación asíncrona.

Validación de formularios en Node.js con Datalize

Es por eso que finalmente decidí escribir mi propia biblioteca de validación de formularios, pequeña pero poderosa, llamada datalize. Es extensible, por lo que puede usarlo en cualquier proyecto y personalizarlo según sus requisitos. Valida el cuerpo, la consulta o los parámetros de una solicitud. También admite filtros async y estructuras JSON complejas como matrices u objetos anidados .

Configuración

Datalize se puede instalar a través de npm:

 npm install --save datalize

Para analizar el cuerpo de una solicitud, debe usar una biblioteca separada. Si aún no usa uno, le recomiendo koa-body para Koa o body-parser para Express.

Puede aplicar este tutorial a su servidor API HTTP ya configurado, o usar el siguiente código de servidor HTTP Koa simple.

 const Koa = require('koa'); const bodyParser = require('koa-body'); const app = new Koa(); const router = new (require('koa-router'))(); // helper for returning errors in routes app.context.error = function(code, obj) { this.status = code; this.body = obj; }; // add koa-body middleware to parse JSON and form-data body app.use(bodyParser({ enableTypes: ['json', 'form'], multipart: true, formidable: { maxFileSize: 32 * 1024 * 1024, } })); // Routes... // connect defined routes as middleware to Koa app.use(router.routes()); // our app will listen on port 3000 app.listen(3000); console.log(' API listening on 3000');

Sin embargo, esta no es una configuración de producción (debe usar el registro, hacer cumplir la autorización, manejar errores, etc.), pero estas pocas líneas de código funcionarán bien para los ejemplos que le mostraré.

Nota: Todos los ejemplos de código usan Koa, pero el código de validación de datos también funcionará para Express. La biblioteca datalize también tiene un ejemplo para implementar la validación de formulario Express.

Un ejemplo de validación de formulario básico de Node.js

Supongamos que tiene un servidor web Koa o Express y un punto final en su API que crea un usuario con varios campos en la base de datos. Algunos campos son obligatorios y otros solo pueden tener valores específicos o deben formatearse para corregir el tipo.

Podrías escribir una lógica simple como esta:

 /** * @api {post} / Create a user * ... */ router.post('/', (ctx) => { const data = ctx.request.body; const errors = {}; if (!String(data.name).trim()) { errors.name = ['Name is required']; } if (!(/^[\-0-9a-zA-Z\.\+_]+@[\-0-9a-zA-Z\.\+_]+\.[a-zA-Z]{2,}$/).test(String(data.email))) { errors.email = ['Email is not valid.']; } if (Object.keys(errors).length) { return ctx.error(400, {errors}); } const user = await User.create({ name: data.name, email: data.email, }); ctx.body = user.toJSON(); });

Ahora reescribamos este código y validemos esta solicitud usando datalize:

 const datalize = require('datalize'); const field = datalize.field; /** * @api {post} / Create a user * ... */ router.post('/', datalize([ field('name').trim().required(), field('email').required().email(), ]), (ctx) => { if (!ctx.form.isValid) { return ctx.error(400, {errors: ctx.form.errors}); } const user = await User.create(ctx.form); ctx.body = user.toJSON(); });

Más corto, más limpio, tan fácil de leer. Con datalize, puede especificar una lista de campos y encadenarles tantas reglas (funciones que arrojan un error si la entrada no es válida) o filtros (funciones que dan formato a la entrada) como desee.

Las reglas y los filtros se ejecutan en el mismo orden en que se definen, por lo que si primero desea recortar una cadena para los espacios en blanco y luego verificar si tiene algún valor, debe definir .trim() antes .required() .

Datalize luego creará un objeto (disponible como .form en el objeto de contexto más amplio) con solo los campos que haya especificado, para que no tenga que enumerarlos nuevamente. La propiedad .form.isValid le dice si la validación fue exitosa o no.

Manejo automático de errores

Si no queremos verificar si el formulario es válido o no con cada solicitud, podemos agregar un middleware global que cancele la solicitud si los datos no pasaron la validación.

Para hacer esto, solo agregamos este fragmento de código a nuestro archivo de arranque donde creamos nuestra instancia de la aplicación Koa/Express.

 const datalize = require('datalize'); // set datalize to throw an error if validation fails datalize.set('autoValidate', true); // only Koa // add to very beginning of Koa middleware chain app.use(async (ctx, next) => { try { await next(); } catch (err) { if (err instanceof datalize.Error) { ctx.status = 400; ctx.body = err.toJSON(); } else { ctx.status = 500; ctx.body = 'Internal server error'; } } }); // only Express // add to very end of Express middleware chain app.use(function(err, req, res, next) { if (err instanceof datalize.Error) { res.status(400).send(err.toJSON()); } else { res.send(500).send('Internal server error'); } });

Y ya no tenemos que verificar si los datos son válidos, ya que datalize lo hará por nosotros. Si los datos no son válidos, devolverá un mensaje de error formateado con una lista de campos no válidos.

Consulta de Validación

Sí, incluso puede validar sus parámetros de consulta muy fácilmente; no tiene que usarse solo con solicitudes POST . Solo usamos el método auxiliar .query() , y la única diferencia es que los datos se almacenan en el objeto .data en lugar de .form .

 const datalize = require('datalize'); const field = datalize.field; /** * @api {get} / List users * ... */ router.post('/', datalize.query([ field('keywords').trim(), field('page').default(1).number(), field('perPage').required().select([10, 30, 50]), ]), (ctx) => { const limit = ctx.data.perPage; const where = { }; if (ctx.data.keywords) { where.name = {[Op.like]: ctx.data.keywords + '%'}; } const users = await User.findAll({ where, limit, offset: (ctx.data.page - 1) * limit, }); ctx.body = users; });

También hay un método auxiliar para la validación de parámetros, .params() . Los datos de consultas y formularios se pueden validar juntos pasando dos middlewares datalize en el método .post() del enrutador.

Más filtros, matrices y objetos anidados

Hasta ahora, hemos utilizado datos realmente simples en nuestra validación de formularios de Node.js. Ahora probemos algunos campos más complejos como matrices, objetos anidados, etc.:

 const datalize = require('datalize'); const field = datalize.field; const DOMAIN_ERROR = "Email's domain does not have a valid MX (mail) entry in its DNS record"; /** * @api {post} / Create a user * ... */ router.post('/', datalize([ field('name').trim().required(), field('email').required().email().custom((value) => { return new Promise((resolve, reject) => { dns.resolve(value.split('@')[1], 'MX', function(err, addresses) { if (err || !addresses || !addresses.length) { return reject(new Error(DOMAIN_ERROR)); } resolve(); }); }); }), field('type').required().select(['admin', 'user']), field('languages').array().container([ field('id').required().id(), field('level').required().select(['beginner', 'intermediate', 'advanced']) ]), field('groups').array().id(), ]), async (ctx) => { const {languages, groups} = ctx.form; delete ctx.form.languages; delete ctx.form.groups; const user = await User.create(ctx.form); await UserGroup.bulkCreate(groups.map(groupId => ({ groupId, userId: user.id, }))); await UserLanguage.bulkCreate(languages.map(item => ({ languageId: item.id, userId: user.id, level: item.level, )); });

Si no hay una regla integrada para los datos que necesitamos validar, podemos crear una regla de validación de datos personalizada con el método .custom() (buen nombre, ¿verdad?) y escribir la lógica necesaria allí. Para objetos anidados, existe el método .container() en el que puede especificar una lista de campos de la misma manera que en la función datalize() . Puede anidar contenedores dentro de contenedores o complementarlos con filtros .array() , que convierte valores en matrices. Cuando el filtro .array() se usa sin un contenedor, las reglas o filtros especificados se aplican a cada valor en la matriz.

Entonces .array().select(['read', 'write']) verificaría si cada valor en la matriz es 'read' o 'write' y si alguno no lo es, devolverá una lista de todos los índices con errores Muy bien, ¿eh?

PUT / PATCH

Cuando se trata de actualizar sus datos con PUT / PATCH (o POST ), no tiene que volver a escribir toda su lógica, reglas y filtros. Simplemente agregue un filtro adicional como .optional() o .patch() , que eliminará cualquier campo del objeto de contexto si no se definió en la solicitud. ( .optional() lo hará siempre opcional, mientras que .patch() lo hará opcional solo si el método de solicitud HTTP es PATCH ). Puede agregar este filtro adicional para que funcione tanto para crear como para actualizar datos en su base de datos.

 const datalize = require('datalize'); const field = datalize.field; const userValidator = datalize([ field('name').patch().trim().required(), field('email').patch().required().email(), field('type').patch().required().select(['admin', 'user']), ]); const userEditMiddleware = async (ctx, next) => { const user = await User.findByPk(ctx.params.id); // cancel request here if user was not found if (!user) { throw new Error('User was not found.'); } // store user instance in the request so we can use it later ctx.user = user; return next(); }; /** * @api {post} / Create a user * ... */ router.post('/', userValidator, async (ctx) => { const user = await User.create(ctx.form); ctx.body = user.toJSON(); }); /** * @api {put} / Update a user * ... */ router.put('/:id', userEditMiddleware, userValidator, async (ctx) => { await ctx.user.update(ctx.form); ctx.body = ctx.user.toJSON(); }); /** * @api {patch} / Patch a user * ... */ router.patch('/:id', userEditMiddleware, userValidator, async (ctx) => { if (!Object.keys(ctx.form).length) { return ctx.error(400, {message: 'Nothing to update.'}); } await ctx.user.update(ctx.form); ctx.body = ctx.user.toJSON(); });

Con dos middlewares simples, podemos escribir la mayoría de la lógica para todos los métodos POST / PUT / PATCH . La función userEditMiddleware() verifica si el registro que queremos editar existe y arroja un error en caso contrario. Luego userValidator() realiza la validación de todos los puntos finales. Finalmente, el filtro .patch() eliminará cualquier campo del objeto .form si no está definido y si el método de solicitud es PATCH .

Extras de validación de formularios de Node.js

En los filtros personalizados, puede obtener valores de otros campos y realizar una validación basada en eso. También puede obtener cualquier dato del objeto de contexto, como la solicitud o la información del usuario, ya que todo se proporciona en los parámetros de devolución de llamada de la función personalizada.

La biblioteca cubre un conjunto básico de reglas y filtros, pero puede registrar filtros globales personalizados que puede usar con cualquier campo, para que no tenga que escribir el mismo código una y otra vez:

 const datalize = require('datalize'); const Field = datalize.Field; Field.prototype.date = function(format = 'YYYY-MM-DD') { return this.add(function(value) { const date = value ? moment(value, format) : null; if (!date || !date.isValid()) { throw new Error('%s is not a valid date.'); } return date.format(format); }); }; Field.prototype.dateTime = function(format = 'YYYY-MM-DD HH:mm') { return this.date(format); };

Con estos dos filtros personalizados, puede encadenar sus campos con .date() o .dateTime() para validar la entrada de fecha.

Los archivos también se pueden validar usando datalize: hay filtros especiales solo para archivos como .file() , .mime() y .size() para que no tenga que manejar los archivos por separado.

Comience a escribir mejores API ahora

He estado usando datalize para la validación de formularios de Node.js en varios proyectos de producción, tanto para API pequeñas como grandes. Me ha ayudado a entregar grandes proyectos a tiempo y con menos estrés, haciéndolos más fáciles de leer y mantener. En un proyecto, incluso lo usé para validar datos para mensajes WebSocket escribiendo un contenedor simple alrededor de Socket.IO y el uso fue prácticamente el mismo que definir rutas en Koa, por lo que estuvo bien. Si hay suficiente interés, podría escribir un tutorial para eso también.

Espero que este tutorial te ayude y construya mejores APIs en Node.js, con datos perfectamente validados sin problemas de seguridad o errores internos del servidor. Y lo más importante, espero que le ahorre una tonelada de tiempo que, de lo contrario, tendría que invertir en escribir funciones adicionales para la validación de formularios mediante JavaScript.