Inicio / Artículos

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.

Editor: REACTAPP.TOP10 min de lectura2373 palabras

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.
  • Reduce tu superficie de ataque. Los datos de entrada no verificados son un punto de entrada común para ataques de inyección como la inyección SQL y el scripting entre sitios (XSS). La validación y la sanitización disminuyen esa exposición, aunque complementan en lugar de reemplazar a las consultas parametrizadas y la codificación adecuada de los resultados.
  • Evita fallos confusos. Muchas caídas en entornos de producción se deben a un campo faltante, vacío o mal escrito que se utilizó unas líneas antes del error real.
  • Genera mensajes de error útiles. En lugar de un mensaje genérico como “Algo salió mal”, se puede indicar a los clientes exactamente qué corregir, como “El correo electrónico no es válido” o “La contraseña debe tener al menos 8 caracteres”.
  • 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.
  • La verdadera lógica de negocio, que consiste en registrar al usuario, está oculta debajo del código de validación.
  • Gestionar a mano cada caso límite, como cadenas vacías, 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.
  • Cada cadena de validación se ejecuta como middleware. Cuando se ejecuta 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.
  • Cada regla tiene su turno. Dado que una cadena fallida sigue llamando al siguiente middleware, todas las reglas se ejecutan y se registra cada problema, no solo el primero.
  • 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.
  • Tu código determina el resultado. Si 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() lee req.body, el contenido en formato JSON o de formulario.
    • query() lee req.query, la cadena de consulta después de ?, como en ?page=2.
    • param() lee req.params, los parámetros de ruta como :id en /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 solicitud
    • query(field) para parámetros de la cadena de consulta
  • param(field) para parámetros de ruta
  • Validadores 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 como status que 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 anterior
    • validationResult(req): recopila todos los errores de la solicitud actual
    • errors.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 slug utiliza 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.
  • Campos opcionales que aún se verifican. 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.
  • Un manejador enfocado. El manejador de la ruta tiene dos funciones: informar sobre los errores de validación y guardar la publicación. Toda la lógica relacionada con “¿son estos datos aceptables?” se encuentra en el array de reglas, donde es fácil de leer y modificar.
  • 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.
  • Validadores de unicidad en el lado del servidor con restricciones de base de datos.
  • Almacenar matchedData(req) en lugar del cuerpo bruto.
  • Lecturas relacionadas