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.
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 });
});
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 personalizadaconst 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 } });
}
);
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 |
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);
Validación de diferentes tipos de entrada
- Validar el body (req.body): Para datos JSON en POST/PUT/PATCH. Usa
body()en express-validator o valida el objeto en Joi. - Validar parámetros de URL (req.params): Para IDs y identificadores en la ruta. Usa
param()en express-validator. - Validar query strings (req.query): Para filtros, paginación y parámetros opcionales. Usa
query()en express-validator. - 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 Jestconst 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();
});
});
});
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.
¿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
En la próxima lección exploraremos cómo manejar errores de manera centralizada para crear respuestas de API consistentes y profesionales.