Middleware en Express: patrones y mejores prácticas

Lectura
25 min~9 min lectura
CONCEPTO CLAVE: El middleware en Express es un conjunto de funciones que tienen acceso al objeto de solicitud (req), al objeto de respuesta (res) y al siguiente middleware en el ciclo de solicitud-respuesta de la aplicación. Son el corazón de Express y la base para construir APIs robustas y escalables.

¿Qué es el Middleware en Express?

El middleware es un concepto fundamental en Express que permite procesar las solicitudes HTTP de manera modular y reutilizable. Cada middleware tiene la capacidad de ejecutar código, modificar los objetos request y response, terminar el ciclo de solicitud-respuesta o llamar al siguiente middleware en la pila.

Cuando Express recibe una petición, la procesa a través de una cadena de middleware antes de llegar a la ruta final. Esto nos permite separar responsabilidades como autenticación, validación, logging y manejo de errores en funciones independientes y reutilizables.

Tipos de Middleware en Express

Express soporta varios tipos de middleware, cada uno con un propósito específico dentro de nuestra aplicación.

1. Middleware de Aplicación

Este tipo de middleware se ejecuta en todas las solicitudes que llegan a tu aplicación, sin importar la ruta o el método HTTP.

const express = require('express');
const app = express();

// Middleware de aplicación - se ejecuta en TODAS las peticiones
app.use((req, res, next) => {
  console.log(`[${new Date().toISOString()}] ${req.method} ${req.path}`);
  req.timestamp = Date.now();
  next();
});

app.get('/usuarios', (req, res) => {
  res.json({ usuarios: [] });
});

app.listen(3000);
💡 Consejo: Usa middleware de aplicación para logging global, configuración de headers CORS, parsing de body y otras tareas transversales que afectan a toda tu aplicación.

2. Middleware de Ruta

Se aplica a rutas específicas o grupos de rutas usando path patterns.

// Middleware específico para rutas de /admin
app.use('/admin', (req, res, next) => {
  console.log('Middleware para admin');
  next();
});

app.get('/admin/dashboard', (req, res) => {
  res.json({ mensaje: 'Panel de administración' });
});

// Middleware con parámetros
app.use('/api/:version', (req, res, next) => {
  console.log(`API Version: ${req.params.version}`);
  next();
});

3. Middleware de Manejo de Errores

Tienen cuatro parámetros (err, req, res, next) y se usan exclusivamente para manejar errores.

// MIDDLEWARE DE ERROR - siempre tiene 4 parámetros
app.use((err, req, res, next) => {
  console.error('Error occurred:', err.stack);
  
  const statusCode = err.statusCode || 500;
  const mensaje = err.message || 'Error interno del servidor';
  
  res.status(statusCode).json({
    success: false,
    error: {
      mensaje,
      stack: process.env.NODE_ENV === 'development' ? err.stack : undefined
    }
  });
});
⚠️ Importante: El middleware de manejo de errores SIEMPRE debe tener cuatro parámetros. Express identifica este tipo de middleware por la presencia del parámetro err como primer argumento.

Patrones de Middleware más Utilizados

📌 Patrón 1: Middleware de Autenticación
El middleware de autenticación es crucial para proteger rutas sensibles de tu API. Debe verificar tokens JWT, sesiones u otros mecanismos de autenticación antes de permitir el acceso.
// Middleware de autenticación JWT
const jwt = require('jsonwebtoken');

const authMiddleware = (req, res, next) => {
  const authHeader = req.headers.authorization;
  
  if (!authHeader || !authHeader.startsWith('Bearer ')) {
    return res.status(401).json({
      success: false,
      error: 'Token de autenticación no proporcionado'
    });
  }
  
  const token = authHeader.split(' ')[1];
  
  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    req.user = decoded; // Adjuntar usuario al request
    next();
  } catch (error) {
    return res.status(401).json({
      success: false,
      error: 'Token inválido o expirado'
    });
  }
};

// Uso: proteger rutas específicas
app.get('/perfil', authMiddleware, (req, res) => {
  res.json({ usuario: req.user });
});
📌 Patrón 2: Middleware de Validación
Separar la validación de datos en middleware dedicado mejora la mantenibilidad y permite reutilizar reglas de validación en diferentes rutas.
// Middleware de validación con express-validator
const { body, validationResult } = require('express-validator');

const validarUsuario = [
  body('email')
    .isEmail()
    .withMessage('Email inválido')
    .normalizeEmail(),
  body('password')
    .isLength({ min: 8 })
    .withMessage('La contraseña debe tener al menos 8 caracteres')
    .matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/)
    .withMessage('La contraseña debe contener mayúsculas, minúsculas y números'),
  body('nombre')
    .trim()
    .notEmpty()
    .withMessage('El nombre es requerido')
    .isLength({ max: 50 })
    .withMessage('El nombre no puede exceder 50 caracteres'),
  (req, res, next) => {
    const errores = validationResult(req);
    if (!errores.isEmpty()) {
      return res.status(400).json({
        success: false,
        errores: errores.array().map(e => e.msg)
      });
    }
    next();
  }
];

app.post('/usuarios', validarUsuario, (req, res) => {
  // Los datos ya están validados y sanitizados
  console.log('Datos válidos:', req.body);
  res.status(201).json({ success: true });
});
📌 Patrón 3: Middleware de Autorización Basado en Roles
Después de la autenticación, usa middleware de autorización para verificar permisos específicos.
// Middleware de autorización por roles
const rolesPermitidos = (...roles) => {
  return (req, res, next) => {
    if (!req.user) {
      return res.status(401).json({
        success: false,
        error: 'Autenticación requerida'
      });
    }
    
    if (!roles.includes(req.user.rol)) {
      return res.status(403).json({
        success: false,
        error: 'No tienes permisos para acceder a este recurso'
      });
    }
    
    next();
  };
};

// Rutas protegidas con diferentes roles
app.delete('/usuarios/:id', authMiddleware, rolesPermitidos('admin'), eliminarUsuario);
app.patch('/configuracion', authMiddleware, rolesPermitidos('admin', 'editor'), actualizarConfig);
app.get('/reportes', authMiddleware, rolesPermitidos('admin', 'editor', 'viewer'), verReportes);

Middleware de Terceros Esenciales

Existen varios middlewares de terceros que deberías considerar para cualquier API REST profesional:

MiddlewarePropósitoAlternativa
corsConfigurar Cross-Origin Resource SharingManual con headers
helmetSeguridad mediante headers HTTPConfiguración manual
morganLogging de solicitudes HTTPconsole.log manual
compressionCompresión gzip de respuestas-
express-rate-limitLimitar solicitudes por IP-
// Instalación: npm install cors helmet morgan compression express-rate-limit

const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const morgan = require('morgan');
const compression = require('compression');
const rateLimit = require('express-rate-limit');

const app = express();

// Seguridad: helmet establece headers HTTP seguros
app.use(helmet());

// CORS: permitir solicitudes desde dominios específicos
app.use(cors({
  origin: ['https://mi-frontend.com', 'https://admin.mi-frontend.com'],
  methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization']
}));

// Rate limiting: limitar a 100 solicitudes por cada IP cada 15 minutos
const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutos
  max: 100,
  message: {
    success: false,
    error: 'Demasiadas solicitudes desde esta IP, intenta más tarde'
  }
});
app.use('/api/', limiter);

// Compression: comprimir respuestas
app.use(compression());

// Logging
app.use(morgan('combined'));
💡 Orden importa: El orden en que defines los middlewares es crucial. Primero van los que procesan requests generalistas (como CORS, body parsing), luego los específicos de tu aplicación (autenticación), y finalmente las rutas.

Mejores Prácticas para Middleware

  1. Siempre llama a next() o envía una respuesta: Cada middleware debe terminar el ciclo de request-response llamando a next() o enviando una respuesta. Si olvidas hacerlo, la solicitud quedará colgada indefinidamente.
  2. Mantén los middlewares pequeños y con una sola responsabilidad: En lugar de un middleware gigante que hace todo, crea varios middlewares pequeños y reutilizables que se compongan.
  3. Coloca los middlewares en el orden correcto: Express ejecuta los middlewares en el orden en que se definen. Asegúrate de que parseadores de body vengan antes de los de validación.
  4. Maneja errores asíncronos correctamente: Usa try-catch en funciones asíncronas y pasa los errores al siguiente middleware de error.
  5. Documenta tu middleware: Añade comentarios JSDoc explicando qué hace cada middleware, qué espera en req y qué modifica.
// ❌ MAL: Middleware que hace demasiado
app.use(async (req, res, next) => {
  // Auth, validación, logging, transformaciones... todo junto
  const token = req.headers.authorization;
  const user = await verifyToken(token);
  req.user = user;
  const data = validate(req.body);
  logRequest(req);
  transformData(req);
  next();
});

// ✅ BIEN: Middlewares separados y reutilizables
app.use(logRequest);           // 1. Logging primero
app.use(helmet());             // 2. Seguridad
app.use(cors());               // 3. CORS
app.use(express.json());       // 4. Parsear body
app.use(authMiddleware);       // 5. Autenticación
app.use('/api', apiRoutes);    // 6. Rutas API

Manejo de Errores Asíncronos

Una fuente común de bugs en Express es el manejo de errores en funciones asíncronas. Si no los manejas correctamente, los errores no llegarán al middleware de errores.

// ❌ MAL: Error async que no se captura
app.get('/usuarios/:id', async (req, res, next) => {
  const usuario = await Usuario.findById(req.params.id);
  if (!usuario) {
    throw new Error('Usuario no encontrado'); // ¡Esto NO funciona!
  }
  res.json(usuario);
});

// ✅ BIEN: Wrappers para capturar errores async
const asyncHandler = (fn) => (req, res, next) => {
  Promise.resolve(fn(req, res, next)).catch(next);
};

app.get('/usuarios/:id', asyncHandler(async (req, res, next) => {
  const usuario = await Usuario.findById(req.params.id);
  if (!usuario) {
    const error = new Error('Usuario no encontrado');
    error.statusCode = 404;
    throw error;
  }
  res.json(usuario);
}));

// Alternativa con express-async-handler
const asyncHandler = require('express-async-handler');

app.get('/productos/:id', asyncHandler(async (req, res) => {
  const producto = await Producto.findById(req.params.id);
  if (!producto) {
    throw new HttpError(404, 'Producto no encontrado');
  }
  res.json(producto);
}));
Ver más: Middleware de Logging Avanzado con Winston
const winston = require('winston');
const expressWinston = require('express-winston');

// Middleware de logging para todas las requests
app.use(expressWinston.logger({
  transports: [
    new winston.transports.File({ filename: 'logs/requests.log' }),
    new winston.transports.Console()
  ],
  format: winston.format.combine(
    winston.format.colorize(),
    winston.format.json()
  ),
  meta: true,
  msg: 'HTTP {{req.method}} {{req.url}}',
  expressFormat: true,
  colorize: false,
  // Ignorar logging de health checks
  ignoreRoute: (req) => req.url === '/health'
}));

Middleware para APIs RESTful

Vamos a crear un conjunto completo de middleware para una API REST profesional:

// middlewares/index.js

// Middleware de parsing (debe ir primero)
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true }));

// Middleware de utilidad para respuestas
app.use((req, res, next) => {
  // Método helper para respuestas exitosas
  res.success = (data, statusCode = 200) => {
    return res.status(statusCode).json({
      success: true,
      data
    });
  };
  
  // Método helper para respuestas con paginación
  res.paginated = (data, pagination, statusCode = 200) => {
    return res.status(statusCode).json({
      success: true,
      data,
      pagination
    });
  };
  
  next();
});

// Middleware de logging de requests
app.use((req, res, next) => {
  const inicio = Date.now();
  res.on('finish', () => {
    const duracion = Date.now() - inicio;
    console.log(`[${new Date().toISOString()}] ${req.method} ${req.url} - ${res.statusCode} (${duracion}ms)`);
  });
  next();
});
El middleware es la salsa secreta de Express. Dominar su uso te permitirá crear APIs más limpias, mantenibles y profesionales. Recuerda: cada middleware debe hacer una sola cosa y hacerla bien.
🧠 Quiz: Middleware en Express

¿Cuál es la firma correcta de un middleware de manejo de errores en Express?

  • A) (req, res, next) => {}
  • B) (err, req, res) => {}
  • C) (err, req, res, next) => {}
  • D) (error, request, response) => {}
✅ Respuesta correcta: C) (err, req, res, next) => {}. El middleware de errores siempre debe tener cuatro parámetros, donde el primero es el error. Express identifica este tipo de middleware por la presencia del parámetro 'err'.
🧠 Quiz: Middleware en Express

¿Qué ocurre si un middleware no llama a next() ni envía una respuesta?

  • A) La solicitud continúa normalmente
  • B) Express lanza un error automáticamente
  • C) La solicitud queda colgada indefinidamente
  • D) Se llama automáticamente al siguiente middleware
✅ Respuesta correcta: C) La solicitud queda colgada indefinidamente. Cada middleware debe terminar el ciclo de request-response llamando a next() o enviando una respuesta (res.send(), res.json(), etc.).
🧠 Quiz: Middleware en Express

¿En qué orden se deben colocar los middlewares en Express?

  • A) Primero específicos, luego generalistas
  • B) El orden no importa
  • C) Primero generalistas, luego específicos
  • D) Solo puede haber un middleware por aplicación
✅ Respuesta correcta: C) Primero generalistas, luego específicos. El orden es crucial: parseadores de body primero, luego seguridad (CORS, helmet), autenticación y finalmente las rutas específicas.
💡 Resumen final: El middleware es un patrón poderoso que te permite separar preocupaciones, reutilizar código y mantener tu código organizado. Practica creando middlewares pequeños y enfocados, y compónlos según las necesidades de tu aplicación.