Saltar al contenido

Lección 6 de 12 · Lectura y práctica

Validación de datos con Joi y express-validator

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.

Del concepto a la práctica

Taller de esta lección

Validar no es solo comprobar que exista un campo: también hay que fijar tipo, límites y transformación permitida antes de guardar. Para inscripciones a una sesión, email debe tener formato razonable, edad debe ser entero entre 16 y 120 y nombre no debe quedar compuesto únicamente por espacios.

Ejemplo resuelto

Un esquema conceptual puede exigir { nombre: cadena no vacía, email: correo, edad: entero 16..120 }. La entrada { "nombre":"Ana", "email":"ana@ejemplo.test", "edad":"22" } solo es aceptable si la capa elegida convierte explícitamente la edad a 22; { "edad":"veintidós" } debe producir 400 y no llegar al repositorio. Devuelve errores por campo, sin guardar parcialmente.

Pruébalo

  1. Define reglas para los tres campos.

  2. Decide si conviertes cadenas numéricas o las rechazas.

  3. Envía un caso válido y tres inválidos.

  4. Comprueba que el almacenamiento no cambia tras cada rechazo.

Error frecuente: validar en el cliente y confiar en ese resultado. Corrígelo repitiendo las reglas en el servidor, que es la frontera que recibe datos no confiables.

Comprueba tu resultado

El caso válido produce 201 y un registro. El correo incorrecto, la edad 15 y el nombre vacío producen 400; cada respuesta identifica el campo y el total de registros permanece igual después de los tres fallos.

Navegador, API y base de datos conectados por solicitudes, consultas y respuestas en sentidos opuestos.
CursaloMapa de práctica
Del navegador a los datos

El navegador envía una solicitud a la API. El servidor valida la operación y consulta los datos; después devuelve una respuesta que el navegador puede mostrar. La base de datos se comunica con el servidor y no directamente con el navegador.

Ver la infografía en tamaño completo ↗
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: 'test@example.com',
          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.

Sigue con Construcción de APIs RESTful con Node.js y Express

Esta lección forma parte de un curso de Cursalo. Revisa su programa y elige entre la compra individual o el acceso a toda la escuela.

Si ya tenías acceso a este curso, entra con tu cuenta: tus derechos y tu progreso se conservan. No necesitas comprarlo de nuevo.

01 / ELEGIR UNA RUTA

El próximo paso

Aprende IA mientras construyes algo que sí necesitas.

Empieza por el curso inicial o explora el catálogo para encontrar una práctica alineada con tu trabajo.