Domina los Middleware en Express.js

El middleware en Express.js es una función que intercepta las peticiones HTTP antes de llegar a las rutas. Funciona como una cadena de procesamiento donde cada función puede modificar req/res, ejecutar lógica personalizada o pasar el control al siguiente middleware.

  Capacidades del Middleware

  • Modificar objetos: Alterar req y res agregando propiedades o métodos
  • Ejecutar lógica: Validaciones, autenticación, logging, parsing de datos
  • Controlar flujo: Terminar la respuesta o continuar con next()
  • Manejo de errores: Capturar y procesar errores de la aplicación

  Concepto Clave

Los middlewares actúan como interceptores que procesan cada petición HTTP de forma secuencial. Se ejecutan en el orden definido y permiten implementar funcionalidades transversales como autenticación, CORS, parsing de JSON, logging y manejo de errores sin duplicar código en cada ruta.

¿Cómo Funciona el Middleware?

Un middleware en Express.js recibe exactamente 3 parámetros y se ejecuta en el flujo de procesamiento de peticiones HTTP:

  Conclusión

  • req (request): El objeto que contiene información sobre la petición HTTP entrante.
  • res (response): El objeto que se utiliza para enviar la respuesta HTTP.
  • next (función): La función que se llama para pasar el control al siguiente middleware en la cadena.

javascript
// Estructura básica de un middleware
function miMiddleware(req, res, next) {
  // Código que se ejecuta
  console.log('Middleware ejecutándose...');
  
  // Llamar a next() para continuar al siguiente middleware
  next();
}

Ciclo de Vida:

El ciclo de vida de un middleware en Express.js se compone de los siguientes pasos:

  1. Recepción de la petición: El middleware se activa cuando se recibe una petición HTTP.
  2. Ejecución del middleware: Se ejecuta la función del middleware, que puede modificar req y res, y llamar a next() para pasar el control al siguiente middleware.
  3. Pasar el control al siguiente middleware: Si next() es llamado, el control se pasa al siguiente middleware en la cadena. Si no se llama a next(), la petición se considera “atrapada” y no se enviará ninguna respuesta.
  4. Envío de la respuesta: Si se ha alcanzado el final de la cadena de middlewares y no se ha enviado ninguna respuesta, se envía una respuesta al cliente.
javascript
const express = require('express');
const app = express();

// Middleware 1: Logger
const logger = (req, res, next) => {
  console.log(`${req.method} ${req.url}`);
  next(); // Pasa al siguiente
};

// Middleware 2: Auth
const auth = (req, res, next) => {
  const token = req.headers.authorization;
  if (!token) return res.status(401).json({ error: 'Sin token' });
  next();
};

// Ruta con middlewares
app.get('/perfil', logger, auth, (req, res) => {
  res.json({ message: 'Perfil obtenido' });
});

app.listen(3000);

Tipos de Middleware en Express.js

1. Application-Level Middleware

Se aplica a toda la aplicación o a rutas específicas usando app.use() o app.METHOD().

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

// Middleware global - se ejecuta en TODAS las rutas
app.use((req, res, next) => {
  console.log('Timestamp:', Date.now());
  next();
});

// Middleware específico para una ruta
app.use('/users', (req, res, next) => {
  console.log('Acceso a /users');
  next();
});

2. Router-Level Middleware (Middleware de nivel de enrutador)

Similar al application-level, pero se aplica solo a instancias de express.Router().

javascript
const router = express.Router();

// Middleware específico del router
router.use((req, res, next) => {
  console.log('Router middleware ejecutándose');
  next();
});

router.get('/profile', (req, res) => {
  res.send('Perfil de usuario');
});

app.use('/api', router);

3. Error-Handling Middleware

Maneja errores que ocurren durante la ejecución de la aplicación.

javascript
// DEBE tener 4 parámetros: (err, req, res, next)
app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(500).send('¡Algo salió mal!');
});

4. Built-in Middleware

Middleware incluido con Express.js.

javascript
// Servir archivos estáticos
app.use(express.static('public'));

// Parser JSON
app.use(express.json());

// Parser URL-encoded
app.use(express.urlencoded({ extended: true }));

5. Third-party Middleware

Middleware creado por la comunidad.

javascript
const morgan = require('morgan');
const cors = require('cors');
const helmet = require('helmet');

app.use(morgan('combined')); // Logging
app.use(cors()); // CORS
app.use(helmet()); // Seguridad

10 Ejemplos Básicos de Middleware

Ejemplo 1: Logger Simple

javascript
// Middleware que registra información de cada petición
const logger = (req, res, next) => {
  const timestamp = new Date().toISOString();
  console.log(`[${timestamp}] ${req.method} ${req.url}`);
  next(); // ¡Importante! Continúa al siguiente middleware
};

app.use(logger);

app.get('/', (req, res) => {
  res.send('¡Hola Mundo!');
});

Explicación: Este middleware se ejecuta antes de cada ruta y registra la fecha, método HTTP y URL de cada petición.

Ejemplo 2: Autenticación Simple

javascript
// Middleware que verifica si el usuario está autenticado
const requireAuth = (req, res, next) => {
  const token = req.headers.authorization;
  
  if (!token) {
      return res.status(401).json({ error: 'Token requerido' });
  }
  
  if (token !== 'Bearer mi-token-secreto') {
      return res.status(403).json({ error: 'Token inválido' });
  }
  
  next(); // Usuario autenticado, continuar
};

// Aplicar solo a rutas protegidas
app.get('/protected', requireAuth, (req, res) => {
  res.json({ message: 'Acceso autorizado' });
});

Explicación: Verifica que el usuario tenga un token válido antes de acceder a rutas protegidas.

Ejemplo 3: Validador de Datos

javascript
// Middleware que valida datos del cuerpo de la petición
const validateUser = (req, res, next) => {
  const { name, email } = req.body;
  
  if (!name || name.length < 2) {
      return res.status(400).json({ 
          error: 'Nombre es requerido y debe tener al menos 2 caracteres' 
      });
  }
  
  if (!email || !email.includes('@')) {
      return res.status(400).json({ 
          error: 'Email válido es requerido' 
      });
  }
  
  next(); // Datos válidos, continuar
};

app.post('/users', express.json(), validateUser, (req, res) => {
  res.json({ message: 'Usuario creado exitosamente' });
});

Explicación: Valida que los datos enviados en el cuerpo de la petición cumplan con ciertos criterios.

Ejemplo 4: Contador de Visitas

javascript
// Middleware que cuenta las visitas a cada ruta
let visitCount = 0;

const countVisits = (req, res, next) => {
  visitCount++;
  req.visitNumber = visitCount;
  console.log(`Visita número: ${visitCount}`);
  next();
};

app.use(countVisits);

app.get('/stats', (req, res) => {
  res.json({ 
      message: `Esta es la visita número ${req.visitNumber}`,
      totalVisits: visitCount 
  });
});

Explicación: Lleva un registro del número total de peticiones y añade esta información al objeto request.

Ejemplo 5: Control de Tiempo de Respuesta

javascript
// Middleware que mide el tiempo de respuesta
const responseTime = (req, res, next) => {
  const startTime = Date.now();
  
  // Interceptar el método end() de la respuesta
  const originalEnd = res.end;
  res.end = function(...args) {
      const duration = Date.now() - startTime;
      console.log(`${req.method} ${req.url} - ${duration}ms`);
      originalEnd.apply(this, args);
  };
  
  next();
};

app.use(responseTime);

app.get('/slow', (req, res) => {
  // Simular operación lenta
  setTimeout(() => {
      res.send('Respuesta después de 2 segundos');
  }, 2000);
});

Explicación: Mide cuánto tiempo toma procesar cada petición desde que llega hasta que se envía la respuesta.

Ejemplo 6: Middleware de CORS Manual

javascript
// Middleware personalizado para manejar CORS
const customCORS = (req, res, next) => {
  res.header('Access-Control-Allow-Origin', '*');
  res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
  res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
  
  // Manejar preflight requests (OPTIONS)
  if (req.method === 'OPTIONS') {
      return res.sendStatus(200);
  }
  
  next();
};

app.use(customCORS);

app.get('/api/data', (req, res) => {
  res.json({ data: 'Accesible desde cualquier origen' });
});

Explicación: Permite que la API sea accesible desde diferentes dominios configurando los headers CORS apropiados.

Ejemplo 7: Cache Simple

javascript
// Middleware de cache simple
const cache = {};

const simpleCache = (req, res, next) => {
  const key = req.originalUrl;
  
  // Si existe en cache, devolver datos cacheados
  if (cache[key]) {
      console.log('Datos desde cache');
      return res.json(cache[key]);
  }
  
  // Interceptar res.json para guardar en cache
  const originalJson = res.json;
  res.json = function(data) {
      cache[key] = data;
      console.log('Datos guardados en cache');
      originalJson.call(this, data);
  };
  
  next();
};

app.get('/expensive-operation', simpleCache, (req, res) => {
  // Simular operación costosa
  setTimeout(() => {
      res.json({ 
          result: 'Operación completada',
          timestamp: new Date().toISOString()
      });
  }, 3000);
});

Explicación: Guarda las respuestas en memoria para servirlas más rápido en peticiones futuras a la misma URL.

Ejemplo 8: Rate Limiting (Limitador de Velocidad)

javascript
// Middleware simple de rate limiting
const requests = {};

const rateLimiter = (limit = 10, windowMs = 60000) => {
  return (req, res, next) => {
      const ip = req.ip || req.connection.remoteAddress;
      const now = Date.now();
      
      // Limpiar requests antiguos
      if (!requests[ip]) {
          requests[ip] = [];
      }
      
      requests[ip] = requests[ip].filter(time => now - time < windowMs);
      
      // Verificar límite
      if (requests[ip].length >= limit) {
          return res.status(429).json({ 
              error: 'Demasiadas peticiones. Intenta más tarde.' 
          });
      }
      
      requests[ip].push(now);
      next();
  };
};

app.use(rateLimiter(5, 60000)); // 5 requests por minuto

app.get('/limited', (req, res) => {
  res.json({ message: 'Petición exitosa' });
});

Explicación: Limita el número de peticiones que un cliente puede hacer en un período de tiempo específico.

Ejemplo 9: Detector de User Agent

javascript
// Middleware que detecta el tipo de cliente
const detectClient = (req, res, next) => {
  const userAgent = req.headers['user-agent'] || '';
  
  req.clientInfo = {
      isMobile: /Mobile|Android|iPhone|iPad/i.test(userAgent),
      isBot: /bot|crawl|spider|scraping/i.test(userAgent),
      browser: 'Desconocido'
  };
  
  // Detectar navegador
  if (userAgent.includes('Chrome')) req.clientInfo.browser = 'Chrome';
  else if (userAgent.includes('Firefox')) req.clientInfo.browser = 'Firefox';
  else if (userAgent.includes('Safari')) req.clientInfo.browser = 'Safari';
  
  console.log(`Cliente: ${req.clientInfo.browser}, Móvil: ${req.clientInfo.isMobile}`);
  next();
};

app.use(detectClient);

app.get('/client-info', (req, res) => {
  res.json({
      message: 'Información del cliente',
      clientInfo: req.clientInfo
  });
});

Explicación: Analiza el User-Agent para determinar qué tipo de dispositivo y navegador está usando el cliente.

Ejemplo 10: Manejo de Errores Personalizado

javascript
// Middleware de manejo de errores (debe ir al final)
const errorHandler = (err, req, res, next) => {
  console.error('Error capturado:', err.message);
  
  // Error de validación
  if (err.name === 'ValidationError') {
      return res.status(400).json({
          error: 'Error de validación',
          details: err.message
      });
  }
  
  // Error de base de datos
  if (err.code === 11000) {
      return res.status(409).json({
          error: 'Recurso duplicado',
          details: 'Este elemento ya existe'
      });
  }
  
  // Error genérico del servidor
  res.status(500).json({
      error: 'Error interno del servidor',
      message: 'Algo salió mal. Por favor, intenta más tarde.'
  });
};

// Ruta que puede generar error
app.get('/error-test', (req, res, next) => {
  const shouldError = req.query.error === 'true';
  
  if (shouldError) {
      const error = new Error('Error de prueba');
      error.name = 'ValidationError';
      return next(error); // Pasar error al middleware de manejo
  }
  
  res.json({ message: 'Sin errores' });
});

// Aplicar middleware de errores (SIEMPRE al final)
app.use(errorHandler);

Explicación: Maneja diferentes tipos de errores de forma centralizada, proporcionando respuestas apropiadas según el tipo de error.

Aplicación Completa de Ejemplo

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

// 1. Middleware global de logging
app.use((req, res, next) => {
  console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`);
  next();
});

// 2. Middleware para parsing JSON
app.use(express.json());

// 3. Middleware de autenticación para rutas específicas
const requireAuth = (req, res, next) => {
  const token = req.headers.authorization;
  if (token === 'Bearer secret') {
      next();
  } else {
      res.status(401).json({ error: 'No autorizado' });
  }
};

// Rutas públicas
app.get('/', (req, res) => {
  res.json({ message: 'API funcionando' });
});

// Rutas protegidas
app.get('/protected', requireAuth, (req, res) => {
  res.json({ message: 'Datos protegidos', user: 'admin' });
});

// 4. Middleware de manejo de errores (al final)
app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(500).json({ error: 'Error interno del servidor' });
});

const PORT = 3000;
app.listen(PORT, () => {
  console.log(`Servidor corriendo en puerto ${PORT}`);
});

Orden de Ejecución

El orden importa mucho en Express.js:

javascript
// 1. Middleware global primero
app.use(logger);

// 2. Middleware específico después
app.use('/api', authMiddleware);

// 3. Rutas después del middleware
app.get('/api/users', getUsers);

// 4. Middleware de errores AL FINAL
app.use(errorHandler);