Validación de datos con Joi y express-validator

Lectura
35 min~8 min lectura
CONCEPTO CLAVE: La validación de datos es la primera línea de defensa contra datos maliciosos o malformados. Sin ella, tu API acepta cualquier entrada, comprometiendo la integridad de tu base de datos y la seguridad de tu aplicación.

Validación de Datos con Joi y Express-Validator

En el desarrollo de APIs RESTful, la validación de datos es un pilar fundamental que no podemos ignorar. Cuando un cliente envía información a tu servidor, nunca debes confiar ciegamente en lo que llega. Tanto Joi como express-validator son bibliotecas poderosas que nos ayudan a validar y sanitizar datos de entrada de manera elegante y mantenible.

¿Por qué necesitamos validación?

Imaginemos que tienes un endpoint para registrar usuarios:

app.post('/api/users', (req, res) => {
  const { email, age } = req.body;
  
  // Sin validación, guardamos directamente
  db.users.create({ email, age });
});

¿Qué sucede si alguien envía age: "treinta y cinco" o email: "no-es-un-email"? Tu base de datos podría almacenar datos inválidos, o peor, tu aplicación podría fallar inesperadamente.

📌 Regla de oro: Nunca confíes en los datos que provienen del cliente. Siempre valida en el servidor, incluso si ya tienes validación en el frontend.

Joi: Validación declarativa y esquemática

Joi es una biblioteca que permite definir esquemas de validación de manera declarativa. Su sintaxis es fluida y expresiva, permitiéndote crear reglas complejas con poco código.

Instalación

npm install joi

Ejemplo práctico: Validación de registro de usuario

const Joi = require('joi');

// Definimos el esquema de validación
const userSchema = Joi.object({
  username: Joi.string()
    .alphanum()
    .min(3)
    .max(30)
    .required(),
  
  email: Joi.string()
    .email()
    .required(),
  
  password: Joi.string()
    .pattern(new RegExp('^[a-zA-Z0-9]{8,30}$'))
    .required(),
  
  age: Joi.number()
    .integer()
    .min(18)
    .max(120)
    .optional(),
  
  roles: Joi.array()
    .items(Joi.string().valid('admin', 'user', 'moderator'))
    .default(['user'])
});

// Middleware de validación
const validateUser = (req, res, next) => {
  const { error, value } = userSchema.validate(req.body, { 
    abortEarly: false // Muestra todos los errores, no solo el primero
  });
  
  if (error) {
    return res.status(400).json({
      error: 'Datos de usuario inválidos',
      details: error.details.map(d => d.message)
    });
  }
  
  // Reemplazamos req.body con los datos validados y transformados
  req.validatedBody = value;
  next();
};

// Uso en la ruta
app.post('/api/users', validateUser, (req, res) => {
  // req.validatedBody contiene los datos ya validados
  console.log('Datos válidos:', req.validatedBody);
  res.json({ message: 'Usuario creado', user: req.validatedBody });
});
💡 Tip profesional: Usar abortEarly: false es recomendable porque le da al usuario información completa sobre todos los errores de una sola vez, en lugar de corregir uno y volver a intentarlo repetidamente.

Validación avanzada con Joi

Ver más: Validación condicional y personalizada
const schema = Joi.object({
  // Validación condicional basada en otro campo
  membershipType: Joi.string().valid('free', 'premium'),
  paymentMethod: Joi.string().when('membershipType', {
    is: 'premium',
    then: Joi.required(),
    otherwise: Joi.forbidden()
  }),
  
  // Validación personalizada
  customSlug: Joi.string().custom((value, helpers) => {
    if (!/^[a-z0-9-]+$/.test(value)) {
      return helpers.error('any.custom');
    }
    return value;
  }),
  
  // Referencia a otros campos
  startDate: Joi.date(),
  endDate: Joi.date().greater(Joi.ref('startDate'))
});

Express-Validator: Integración nativa con Express

Express-validator está diseñado específicamente para integrarse con el flujo de middlewares de Express. Utiliza una sintaxis basada en cadenas y arrays de validadores, lo que puede resultar más familiar para quienes vienen de frameworks como Laravel o CodeIgniter.

Instalación

npm install express-validator

Ejemplo práctico: Validación de producto

const { body, param, query, validationResult } = require('express-validator');

// Middleware que maneja los resultados de validación
const handleValidationErrors = (req, res, next) => {
  const errors = validationResult(req);
  if (!errors.isEmpty()) {
    return res.status(400).json({ 
      errors: errors.array() 
    });
  }
  next();
};

// Validaciones para crear un producto
app.post('/api/products',
  [
    // Validación del body
    body('name')
      .trim()
      .notEmpty().withMessage('El nombre es obligatorio')
      .isLength({ min: 3, max: 100 })
      .withMessage('El nombre debe tener entre 3 y 100 caracteres'),
    
    body('price')
      .isFloat({ min: 0.01 })
      .withMessage('El precio debe ser un número mayor a 0'),
    
    body('category')
      .isIn(['electronics', 'clothing', 'books', 'home'])
      .withMessage('Categoría inválida'),
    
    body('tags')
      .optional()
      .isArray()
      .withMessage('Las etiquetas deben ser un array'),
    
    // Validación de parámetros
    param('userId')
      .isInt()
      .withMessage('El ID de usuario debe ser un número entero'),
    
    // Validación de query strings
    query('page')
      .optional()
      .isInt({ min: 1 })
      .toInt()
  ],
  handleValidationErrors,
  (req, res) => {
    // Si llegamos aquí, los datos están validados
    const { name, price, category, tags } = req.body;
    res.json({ message: 'Producto creado', product: { name, price, category, tags } });
  }
);
⚠️ Advertencia: Recuerda siempre llamar a handleValidationErrors después de tus validaciones. Sin este middleware, los errores simplemente se acumulan en el objeto req pero no se procesan, y tu endpoint continuará ejecutándose con datos no validados.

Validadores más comunes en express-validator

Validador Descripción Ejemplo
isEmail() Valida formato de email body('email').isEmail()
isLength() Valida longitud de string .isLength({ min: 5, max: 50 })
isInt() Valida que sea entero .isInt({ min: 0, max: 150 })
isIn() Valida que esté en lista .isIn(['draft', 'published'])
matches() Valida con regex .matches(/^[A-Z]/)
trim() Limpia espacios en blanco .trim().notEmpty()
toInt() Convierte a entero .isInt().toInt()

Comparación: ¿Cuándo usar cada uno?

"La mejor herramienta es la que se adapta a tu proyecto y que tu equipo pueda mantener fácilmente."
Característica Joi Express-Validator
Curva de aprendizaje Moderada - sintaxis fluida Baja - similar a otros frameworks
Esquemas reutilizables ⭐ Excelente (objetos Joi) ⚡ Moderado (requiere funciones wrapper)
Integración Express Requiere middleware custom ⭐ Nativa
Transformación de datos ⭐ Muy potente Básica
Documentación Completa y clara Buena
📌 Recomendación: Si trabajas principalmente con Express y necesitas validación rápida, express-validator es una excelente opción. Si necesitas esquemas complejos y reutilizables, o trabajas con múltiples frameworks, Joi ofrece mayor flexibilidad.

Middleware reutilizable: Patrón profesional

Para mantener tu código limpio y DRY (Don't Repeat Yourself), puedes crear middlewares genéricos de validación:

// validators/createUserValidator.js
const Joi = require('joi');

const schema = Joi.object({
  name: Joi.string().min(2).max(100).required(),
  email: Joi.string().email().required(),
  password: Joi.string().min(8).required()
});

const createUserValidator = (req, res, next) => {
  const { error, value } = schema.validate(req.body, { abortEarly: false });
  
  if (error) {
    return res.status(400).json({
      success: false,
      errors: error.details.map(d => ({
        field: d.path.join('.'),
        message: d.message
      }))
    });
  }
  
  req.validatedBody = value;
  next();
};

module.exports = createUserValidator;

// Uso en tus rutas
const createUserValidator = require('./validators/createUserValidator');

app.post('/api/users', createUserValidator, userController.create);
app.post('/api/auth/register', createUserValidator, authController.register);
💡 Patrón de diseño: Separa tus validadores en archivos dedicados. Esto facilita las pruebas unitarias y permite reutilizarlos en diferentes endpoints o incluso en scripts de migración.

Validación de diferentes tipos de entrada

  1. Validar el body (req.body): Para datos JSON en POST/PUT/PATCH. Usa body() en express-validator o valida el objeto en Joi.
  2. Validar parámetros de URL (req.params): Para IDs y identificadores en la ruta. Usa param() en express-validator.
  3. Validar query strings (req.query): Para filtros, paginación y parámetros opcionales. Usa query() en express-validator.
  4. Validar headers (req.headers): Para autenticación o información del cliente. Usa header() en express-validator.
// Ejemplo completo de validación multi-nivel
app.get('/api/users/:userId/posts',
  [
    // Validar parámetro de ruta
    param('userId')
      .isInt()
      .withMessage('ID de usuario inválido')
      .toInt(),
    
    // Validar query parameters
    query('page')
      .optional()
      .isInt({ min: 1 })
      .toInt()
      .default(1),
    
    query('limit')
      .optional()
      .isInt({ min: 1, max: 100 })
      .toInt()
      .default(10),
    
    query('sort')
      .optional()
      .isIn(['date', 'title', 'views'])
      .default('date')
  ],
  handleValidationErrors,
  async (req, res) => {
    const { userId } = req.params;
    const { page, limit, sort } = req.query;
    
    // Todos los datos están validados y transformados
    const posts = await Post.findAll({
      where: { userId },
      limit,
      offset: (page - 1) * limit,
      order: [[sort, 'DESC']]
    });
    
    res.json({ posts });
  }
);

Pruebas de validación

Ver más: Ejemplo de pruebas con Jest
const Joi = require('joi');
const { validateUser } = require('../validators/userValidator');

describe('User Validator', () => {
  describe('validateUser', () => {
    it('debe pasar con datos válidos', () => {
      const req = {
        body: {
          username: 'testuser',
          email: '[email protected]',
          password: 'Password123'
        }
      };
      const res = {
        status: jest.fn().mockReturnThis(),
        json: jest.fn()
      };
      const next = jest.fn();
      
      validateUser(req, res, next);
      
      expect(next).toHaveBeenCalled();
      expect(req.validatedBody).toBeDefined();
    });
    
    it('debe rechazar email inválido', () => {
      const req = {
        body: {
          username: 'testuser',
          email: 'no-es-email',
          password: 'Password123'
        }
      };
      const res = {
        status: jest.fn().mockReturnThis(),
        json: jest.fn()
      };
      const next = jest.fn();
      
      validateUser(req, res, next);
      
      expect(res.status).toHaveBeenCalledWith(400);
      expect(next).not.toHaveBeenCalled();
    });
  });
});
⚠️ Seguridad: Nunca devuelvas detalles internos de errores en producción (como stack traces). Los mensajes de error de validación deben ser claros para el usuario pero no revelar la estructura interna de tu aplicación.

Resumen de conceptos clave

  • La validación de datos es esencial para la seguridad y estabilidad de tu API.
  • Joi ofrece validación declarativa con esquemas potentes y reutilizables.
  • Express-validator se integra naturalmente con el sistema de middlewares de Express.
  • Siempre devuelve errores significativos que ayuden al usuario a corregir sus datos.
  • Separa tus validadores en módulos para facilitar mantenimiento y pruebas.
  • Combina validación en servidor con validación en cliente para mejor UX.
🧠 Quiz

¿Cuál es la principal diferencia entre Joi y express-validator en términos de integración?

  • A) Joi es más rápido que express-validator
  • B) Joi requiere configuración adicional para Express, mientras express-validator está diseñado nativamente para middlewares de Express
  • C) Express-validator solo funciona con bases de datos SQL
  • D) No hay diferencias significativas
✅ Respuesta correcta: B) Joi es una biblioteca de validación más general que puede usarse con cualquier framework de JavaScript, por lo que requiere crear middlewares personalizados para integrarse con Express. Express-validator, en cambio, está específicamente diseñado para el sistema de middlewares de Express, ofreciendo una integración más directa.

En la próxima lección exploraremos cómo manejar errores de manera centralizada para crear respuestas de API consistentes y profesionales.