Validación de entrada del lado del servidor en Express con cadenas de express-validator
Aprenda por qué las APIs Express deben validar la entrada en el servidor, cómo las cadenas de express-validator registran errores para validationResult y cómo estructurar una ruta de producción.
Fuentes: Stop Trusting User Input: A Beginner’s Guide to `express-validator` in Express.js
Tarde o temprano, toda API Express que acepta entrada recibe algo como "age": "banana", y el servidor o bien se cae o, peor aún, lo almacena. La validación es la capa que evita esto, y realizarla adecuadamente en el backend es importante sin importar cuán cuidadosos sean los formularios del frontend. Esta guía explica qué es la validación y por qué el servidor debe encargarse de ella, muestra el enfoque manual y dónde falla, y luego explica cómo funciona internamente express-validator, qué métodos utilizarás con más frecuencia, y cómo una ruta realista combina validación, saneamiento y verificaciones basadas en la base de datos.
Qué significa la validación
La validación es el acto de confirmar que los datos recibidos tienen la estructura y el contenido esperados antes de que cualquier parte del código actúe sobre ellos. Una analogía útil es la verificación de identidad en un lugar: no importa cuán seguro parezca alguien, el personal verifica su documento de identidad y, si no cumple los requisitos, no puede entrar. La validación aplica el mismo escrutinio a cada campo de una solicitud. ¿Es esto realmente una dirección de correo electrónico? ¿Es este realmente un número? ¿Existe siquiera ese campo?
Considere un punto de extremo para registro. Un cuerpo de solicitud bien formado podría verse así:
{
"name": "Rahul Sharma",
"email": "rahul@example.com",
"age": 25,
"password": "supersecret123"
}
Validarlo significa confirmar que email está formateado como una dirección de correo electrónico, que age es un número y no texto, y que password cumple con una longitud mínima que la haga más que una mera formalidad.
Por qué el backend no puede confiar en las verificaciones del frontend
Una objeción común es que el formulario de registro ya marca los campos como required y verifica el formato del correo electrónico en el navegador. Eso no es suficiente, porque desde el punto de vista de un atacante la interfaz frontal es opcional. Cualquiera puede llamar directamente a su API con Postman, curl o un script y eludir todas las reglas de su interfaz de usuario. Si el servidor no verifica los datos en sí, en realidad está confiando en quien envía la solicitud.
Los beneficios concretos de la validación del lado del servidor:
- Protege sus datos. Cuando una columna espera un número y recibe
"twenty-five", o bien se genera un error o, peor aún, se crea un registro mal formado que causa problemas más adelante.
La división del trabajo es sencilla: la validación en el frontend existe para ayudar a los usuarios, mientras que la validación en el backend sirve para proteger la aplicación. Ambas son valiosas, pero solo la capa de backend es obligatoria.
Validación manual y por qué impide la escalabilidad
Vale la pena ver primero la versión manual. Aquí está el inicio de una ruta de registro que obtiene los campos del cuerpo analizado (esto supone que está registrado un analizador de cuerpos JSON como express.json()):
app.post("/register", (req, res) => {
const { name, email, age, password } = req.body;
Cada campo recibe entonces su propia cláusula de protección que devuelve una respuesta 400:
if (!name || typeof name !== "string" || name.trim().length === 0) {
return res.status(400).json({ error: "Name is required" });
} if (!email || !email.includes("@")) {
return res.status(400).json({ error: "A valid email is required" });
} if (!age || typeof age !== "number" || age < 18) {
return res.status(400).json({ error: "Age must be a number and at least 18" });
} if (!password || password.length < 8) {
return res.status(400).json({ error: "Password must be at least 8 characters" });
} // If we made it here, the data is safe to use
res.json({ message: "User registered successfully" });
});
Para una aplicación pequeña esto es perfectamente aceptable. Sin embargo, a medida que crece el código, se acumulan los problemas:
- Cada ruta que acepta entrada necesita su propio bloque de instrucciones
if. - Las verificaciones se copian entre rutas, y una corrección aplicada en una copia puede pasarse fácilmente por alto en las demás.
null, tipos incorrectos y valores demasiado cortos o largos, se vuelve rápidamente tedioso.La versión manual también presenta lagunas sutiles. email.includes("@") acepta muchas direcciones inválidas, y llamar a .includes sobre un valor que no sea cadena genera errores. Este tipo de verificaciones repetitivas y propensas a errores es precisamente lo que debería manejar una biblioteca.
Qué ofrece express-validator
express-validator es un conjunto de middleware para Express desarrollado sobre la biblioteca validator.js. En lugar de escribir lógica condicional para cada campo, se declaran reglas y la biblioteca evalúa los datos de la solicitud en función de ellas. Comience importando las dos funciones principales:
const { body, validationResult } = require("express-validator");
La misma ruta de registro, reescrita con reglas declarativas, se ve así:
app.post(
"/register",
[
body("name").trim().notEmpty().withMessage("Name is required"),
body("email").isEmail().withMessage("A valid email is required"),
body("age").isInt({ min: 18 }).withMessage("Age must be at least 18"),
body("password").isLength({ min: 8 }).withMessage("Password must be at least 8 characters"),
],
(req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
} res.json({ message: "User registered successfully" });
}
);
Las reglas ahora están redactadas en un lenguaje cercano al natural: el nombre no debe quedar vacío después de eliminar espacios en blanco, el correo electrónico debe ser válido, la edad debe ser un número entero de al menos 18 años, y la contraseña debe tener al menos ocho caracteres. El manejador final solo se ocupa de una cuestión: ¿alguna cosa falló?
¿Qué sucede cuando llega una solicitud?
El malentendido más común entre los principiantes es esperar que express-validator rechace automáticamente las solicitudes inválidas. Esto no ocurre; en su lugar, recopila información sobre el problema y deja la respuesta completamente en tus manos. La secuencia para cada solicitud es:
- Express inicia la cadena de middleware definida para la ruta y ejecuta cada elemento en orden.
body("email").isEmail(), se lee el valor de email de req.body y se comprueba. Un fallo no genera excepción ni termina la solicitud; la cadena registra el error en el objeto de la solicitud y continúa.validationResult(req) lee los errores registrados. Se trata de una función ordinaria, no de middleware, por lo que se llama desde el manejador. Ella misma no realiza ninguna validación; solo informa lo que ya han recopilado las cadenas.errors.isEmpty() devuelve false, envías una respuesta 400 con los detalles. De lo contrario, procedes con el trabajo real.El diseño prioriza la flexibilidad sobre la conveniencia. Dado que el rechazo no es automático, tú controlas el formato y el momento de la respuesta, y puedes ejecutar otra lógica entre las verificaciones y la decisión final. La contrapartida es que olvidar llamar a validationResult() desactiva silenciosamente la validación para esa ruta, por lo que muchos equipos envuelven esta verificación en un pequeño middleware reutilizable.
Los cuatro componentes básicos
Una vez que conoces estas partes, la biblioteca resulta sencilla de usar.
Cadenas de validación
Funciones como body(), query(), param() y check() crean una cadena: una secuencia de reglas unidas con puntos, como body("email").trim().isEmail(). Cada una se dirige a una parte diferente de la solicitud:
body()leereq.body, el contenido en formato JSON o de formulario.query()leereq.query, la cadena de consulta después de?, como en?page=2.param()leereq.params, los parámetros de ruta como:iden/users/:id.check()examina todas las ubicaciones de la solicitud al mismo tiempo.
Validadores
Los validadores son las funciones que prueban un valor: isEmail(), isInt(), isLength(), isAlphanumeric() y muchas más. La mayoría delegan en validator.js, que ya contiene una lógica madura y bien probada para formatos comunes.
Sanitizadores
Los sanitizadores transforman los datos en lugar de simplemente verificarlos. trim() elimina el espacio en blanco adyacente, normalizeEmail() convierte una dirección a su forma canónica, y escape() reemplaza los caracteres significativos en HTML por sus equivalentes de entidad para que no puedan interpretarse como marcado. Los sanitizadores pueden combinarse con los validadores en la misma cadena, y el orden es importante: un trim() colocado antes de notEmpty() hace que un valor compuesto únicamente por espacios falle la verificación.
El almacén de resultados por solicitud
Cada vez que se ejecutan las cadenas, sus errores se asocian a ese objeto req específico. validationResult(req) simplemente lee de ese almacenamiento, y por eso necesita el mismo objeto de solicitud que procesaron las cadenas.
En conjunto, el flujo es el siguiente: llega una solicitud, se ejecutan las cadenas y registran silenciosamente los problemas, el manejador solicita los resultados y usted elige la respuesta. Si está comparando este enfoque con la validación basada en esquemas, nuestra comparación de Zod y express-validator analiza los compromisos.
Los métodos que cubren la mayoría de los proyectos reales
No necesita toda la API; un conjunto pequeño maneja la mayoría de los casos.
Seleccionadores de ubicación
body(field)para campos en el cuerpo de la solicitudquery(field)para parámetros de la cadena de consulta
param(field) para parámetros de rutaValidadores comunes
.notEmpty(): el valor no debe estar vacío.isEmail(): el valor debe ser una dirección de correo electrónico válida.isInt({ min, max }): el valor debe ser un número entero, opcionalmente dentro de unos límites.isLength({ min, max }): verifica la longitud de una cadena.isString(): el valor debe ser una cadena de texto.isBoolean(): el valor debe ser un valor booleano.matches(regex): el valor debe coincidir con un patrón personalizado.isIn([...values]): el valor debe ser uno de un conjunto fijo, útil para campos comostatusque puede ser"active"o"inactive"
Sanitizadores comunes
.trim(): elimina los espacios en blanco al principio y al final.escape(): escapa los caracteres HTML para reducir el riesgo de inyección.normalizeEmail(): estandariza el formato de las direcciones de correo electrónico.toInt()y.toFloat(): convierten la entrada de texto en números
Reglas personalizadas
.custom((value, { req }) => { ... }): implementa su propia regla cuando no hay un validador integrado que sirva, como verificar que un nombre de usuario aún no esté registrado
Informes de errores
.withMessage("text"): establece el mensaje para la regla anteriorvalidationResult(req): recopila todos los errores de la solicitud actualerrors.array(): convierte esos errores en un array simple adecuado para una respuesta JSON
Una ruta de estilo de producción
El ejemplo siguiente se acerca más a lo que contiene una API real: una ruta que crea una publicación de blog, combinando validación, saneamiento y una comprobación que consulta la base de datos. Primero están las importaciones, donde Post representa un modelo como un esquema de Mongoose:
const express = require("express");
const { body, validationResult } = require("express-validator");
const Post = require("./models/Post"); // a Mongoose model, for example
Luego está el router y la ruta con su array de reglas:
const router = express.Router();router.post(
"/posts",
[
body("title")
.trim()
.notEmpty().withMessage("Title is required")
.isLength({ max: 100 }).withMessage("Title cannot exceed 100 characters"), body("slug")
.trim()
.notEmpty().withMessage("Slug is required")
.matches(/^[a-z0-9-]+$/).withMessage("Slug can only contain lowercase letters, numbers, and hyphens")
.custom(async (slug) => {
const existing = await Post.findOne({ slug });
if (existing) {
throw new Error("This slug is already taken");
}
return true;
}), body("content")
.trim()
.notEmpty().withMessage("Content cannot be empty")
.isLength({ min: 20 }).withMessage("Content should be at least 20 characters"), body("tags")
.optional()
.isArray().withMessage("Tags must be an array")
.custom((tags) => tags.every((t) => typeof t === "string"))
.withMessage("Each tag must be a string"),
],
async (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
} try {
const post = await Post.create(req.body);
res.status(201).json(post);
} catch (err) {
res.status(500).json({ error: "Something went wrong while saving the post" });
}
}
);module.exports = router;
Varios detalles merecen ser estudiados:
- Reglas respaldadas por la base de datos. La cadena
slugutiliza un validador asíncrono.custom()para buscar publicaciones existentes. Una expresión regular puede confirmar el formato de un slug, pero solo una consulta puede verificar si es único. Lanzar un error dentro de la función personalizada marca el campo como inválido con ese mensaje.
tags está marcado como .optional(), por lo que una publicación sin etiquetas es aceptada, pero cuando se proporcionan etiquetas, estas deben ser un array de cadenas.Dos consideraciones antes de copiar este patrón. Primero, la verificación de unicidad en un validador está sujeta a situaciones competitivas: dos solicitudes con el mismo slug pueden superar la búsqueda antes de que cualquiera sea guardada, por lo que es necesario mantener un índice único en la columna y manejar también el error de clave duplicada. Segundo, pasar req.body directamente a Post.create() guarda todos los campos adicionales que incluyó el cliente, no solo aquellos validados. express-validator proporciona matchedData(req), que devuelve solo los campos cubiertos por tus cadenas de validación, y usarlo soluciona ese problema de asignación masiva.
A medida que la aplicación crece, mueve los arrays de reglas a módulos dedicados, como por ejemplo validators/postValidator.js, e impórtalos en los archivos de rutas, para que estas no se conviertan en montones de código de validación.
Conclusión
La validación no es un elemento opcional; determina si su aplicación puede confiar en los datos que procesa. Las comprobaciones manuales con if son adecuadas para unos pocos campos, pero su rendimiento disminuye rápidamente al aplicarse a muchas rutas.
express-validator no es mágico. Ejecuta las reglas que usted defina como middleware, registra los fallos en la solicitud y permite que validationResult() le entregue un informe para que decida cuál será la respuesta. Teniendo en cuenta este flujo, el resto de la biblioteca consiste en una caja de herramientas con validadores y sanitizadores que puede seleccionar según sea necesario. Los puntos más importantes en la práctica son:
- Trate la validación en el backend como algo obligatorio, independientemente de las comprobaciones en el frontend.
- Siempre llame a
validationResult(), de preferencia desde un middleware compartido, ya que la biblioteca nunca rechaza las solicitudes por sí sola. - Ordene los sanitizadores de manera intencionada dentro de una cadena.
matchedData(req) en lugar del cuerpo bruto.Lecturas relacionadas
- Eliminando el código genérico de try/catch en rutas Express con asyncHandler — Aprenda cómo los middleware de errores, un wrapper asyncHandler y una clase AppError personalizada permiten centralizar las respuestas de error de Express en un único lugar.
- Control de acceso basado en roles en Express con JWT y dos middleware — Aprenda cómo implementar el control de acceso por roles en una API Express combinando middleware de autenticación JWT con un guardián authorize() reutilizable, y cuándo responder con 401 en lugar de 403.