Middlewares en Express para lógica transversal

Capítulo 6

Tiempo estimado de lectura: 8 minutos

+ Ejercicio

Qué es un middleware en Express

Un middleware es una función que se ejecuta durante el ciclo de vida de una petición HTTP, entre el momento en que llega al servidor y el momento en que se envía la respuesta. En Express, un middleware puede:

  • Leer y modificar req (request) y res (response).
  • Terminar la respuesta (por ejemplo, con res.json(...) o res.status(...).send(...)).
  • Delegar el control al siguiente middleware con next().

La idea clave: los middlewares son ideales para lógica transversal (cross-cutting), como logging, validación, cabeceras, autenticación, rate limiting, etc., sin repetir código en cada ruta.

Orden de ejecución y el papel de next()

Express ejecuta middlewares en el orden en que se registran. Ese orden depende de dónde los declares y cómo los apliques (global, router, ruta). Un middleware típico tiene esta firma:

function miMiddleware(req, res, next) {  // ... lógica  next();}

Reglas prácticas sobre next()

  • Si llamas a next(), Express continúa con el siguiente middleware que coincida.
  • Si envías respuesta (res.send, res.json, res.end), normalmente no debes llamar a next() después.
  • Si ocurre un error y quieres delegarlo al manejador de errores, usa next(err).

Visualizando el flujo

Imagina esta cadena:

app.use(mwA);app.use(mwB);app.get('/ruta', mwC, handlerFinal);

Para GET /ruta, el orden será: mwA -> mwB -> mwC -> handlerFinal (siempre que cada uno llame a next() y no corte la respuesta).

Continúa en nuestra aplicación.
  • Escuche el audio con la pantalla apagada.
  • Obtenga un certificado al finalizar.
  • ¡Más de 5000 cursos para que explores!
O continúa leyendo más abajo...
Download App

Descargar la aplicación

Guía práctica paso a paso: middlewares típicos

A continuación implementarás middlewares comunes y los aplicarás a distintos niveles. Los ejemplos asumen que ya tienes una app Express creada y funcionando, y que puedes editar tu archivo principal (por ejemplo app.js o server.js).

Paso 1: Middleware de logging de requests

Objetivo: registrar método, ruta y tiempo de respuesta. Este middleware es global porque suele interesar para todas las peticiones.

// middleware/logRequest.jsfunction logRequest(req, res, next) {  const start = Date.now();  res.on('finish', () => {    const ms = Date.now() - start;    console.log(`${req.method} ${req.originalUrl} - ${res.statusCode} (${ms}ms)`);  });  next();}module.exports = { logRequest };

Aplicación global:

const express = require('express');const { logRequest } = require('./middleware/logRequest');const app = express();app.use(logRequest);

res.on('finish') se dispara cuando la respuesta ya se envió, por eso es útil para medir tiempo y status final.

Paso 2: Middleware para manejo de cabeceras (headers)

Objetivo: añadir cabeceras comunes. Por ejemplo, un header didáctico y permitir CORS básico (en un entorno real podrías usar librerías específicas, pero aquí lo hacemos a mano para entender el concepto).

// middleware/headers.jsfunction setCommonHeaders(req, res, next) {  res.setHeader('X-Powered-By', 'Express');  // CORS básico (didáctico)  res.setHeader('Access-Control-Allow-Origin', '*');  res.setHeader('Access-Control-Allow-Headers', 'Content-Type, X-API-Key');  res.setHeader('Access-Control-Allow-Methods', 'GET,POST,PUT,PATCH,DELETE,OPTIONS');  // Responder preflight OPTIONS sin pasar a rutas  if (req.method === 'OPTIONS') {    return res.sendStatus(204);  }  next();}module.exports = { setCommonHeaders };

Aplicación global (normalmente antes de tus rutas):

const { setCommonHeaders } = require('./middleware/headers');app.use(setCommonHeaders);

Paso 3: Middleware de validación básica

Objetivo: validar datos de entrada sin repetir lógica. Un patrón útil es crear un factory (función que devuelve middleware) para reutilizarlo con diferentes reglas.

Ejemplo: validar que exista un campo en req.body. (Asumiendo que ya usas express.json() en tu app; si no, las validaciones sobre req.body no verán datos).

// middleware/validate.jsfunction requireBodyFields(fields = []) {  return function (req, res, next) {    const missing = fields.filter((f) => req.body?.[f] === undefined);    if (missing.length > 0) {      return res.status(400).json({        error: 'VALIDATION_ERROR',        message: `Faltan campos requeridos: ${missing.join(', ')}`      });    }    next();  };}module.exports = { requireBodyFields };

Uso por ruta (solo donde aplica):

const { requireBodyFields } = require('./middleware/validate');app.post('/users', requireBodyFields(['name', 'email']), (req, res) => {  // aquí ya sabes que name y email existen  res.status(201).json({ ok: true, user: req.body });});

Paso 4: Autenticación simulada con una clave en header

Objetivo: proteger endpoints con un mecanismo simple para fines didácticos. Usaremos un header X-API-Key y compararemos con una clave esperada. No es un sistema de seguridad real, pero sirve para entender el flujo de autenticación con middlewares.

// middleware/auth.jsfunction requireApiKey(expectedKey) {  return function (req, res, next) {    const apiKey = req.header('X-API-Key');    if (!apiKey) {      return res.status(401).json({ error: 'UNAUTHORIZED', message: 'Falta X-API-Key' });    }    if (apiKey !== expectedKey) {      return res.status(403).json({ error: 'FORBIDDEN', message: 'X-API-Key inválida' });    }    // Puedes adjuntar info al request para usarla luego    req.auth = { type: 'apiKey', keyId: 'demo' };    next();  };}module.exports = { requireApiKey };

Uso por router o por ruta (según lo que quieras proteger).

Aplicación de middlewares: global, por router y por ruta

1) Middlewares globales (app.use)

Se ejecutan para todas las rutas que coincidan con el path (si no pasas path, coincide con todo). Útil para logging, headers comunes, parseo, etc.

const express = require('express');const { logRequest } = require('./middleware/logRequest');const { setCommonHeaders } = require('./middleware/headers');const app = express();app.use(express.json());app.use(logRequest);app.use(setCommonHeaders);

2) Middlewares por router (router.use)

Cuando tienes un conjunto de rutas bajo un prefijo (por ejemplo /admin), puedes aplicar middlewares solo a ese grupo.

const express = require('express');const { requireApiKey } = require('./middleware/auth');const adminRouter = express.Router();adminRouter.use(requireApiKey(process.env.ADMIN_API_KEY || 'dev-secret'));adminRouter.get('/stats', (req, res) => {  res.json({ ok: true, auth: req.auth, stats: { users: 10 } });});module.exports = { adminRouter };

Montaje del router:

const { adminRouter } = require('./routes/admin');app.use('/admin', adminRouter);

Resultado: solo las rutas que empiecen por /admin requieren la clave.

3) Middlewares por ruta (en la definición de la ruta)

Útil cuando solo una ruta específica necesita validación o autenticación.

const { requireBodyFields } = require('./middleware/validate');const { requireApiKey } = require('./middleware/auth');app.post('/payments',  requireApiKey(process.env.PAYMENTS_KEY || 'payments-dev'),  requireBodyFields(['amount', 'currency']),  (req, res) => {    res.status(201).json({ ok: true, payment: req.body });  });

El orden importa: primero autenticas, luego validas, luego ejecutas el handler.

Composición sin duplicación: reutiliza cadenas de middlewares

Cuando varias rutas comparten la misma cadena de middlewares, evita copiar y pegar. Dos técnicas comunes:

Técnica A: arreglo de middlewares reutilizable

const { requireApiKey } = require('./middleware/auth');const { requireBodyFields } = require('./middleware/validate');const protectPayments = [  requireApiKey(process.env.PAYMENTS_KEY || 'payments-dev')];const validatePaymentBody = [  requireBodyFields(['amount', 'currency'])];app.post('/payments', ...protectPayments, ...validatePaymentBody, (req, res) => {  res.status(201).json({ ok: true });});app.put('/payments/:id', ...protectPayments, ...validatePaymentBody, (req, res) => {  res.json({ ok: true, id: req.params.id });});

Ventaja: composición flexible y legible.

Técnica B: sub-router con middlewares comunes

Si un conjunto de endpoints comparten protección y/o headers específicos, un router reduce repetición.

const express = require('express');const { requireApiKey } = require('./middleware/auth');const { requireBodyFields } = require('./middleware/validate');const paymentsRouter = express.Router();paymentsRouter.use(requireApiKey(process.env.PAYMENTS_KEY || 'payments-dev'));paymentsRouter.post('/', requireBodyFields(['amount', 'currency']), (req, res) => {  res.status(201).json({ ok: true, payment: req.body });});paymentsRouter.put('/:id', requireBodyFields(['amount', 'currency']), (req, res) => {  res.json({ ok: true, id: req.params.id, payment: req.body });});module.exports = { paymentsRouter };

Montaje:

const { paymentsRouter } = require('./routes/payments');app.use('/payments', paymentsRouter);

Ventaja: el middleware de autenticación se declara una sola vez para todo el grupo.

Manejo de errores con middleware (patrón esencial)

Además de middlewares “normales”, Express soporta middlewares de error con 4 parámetros: (err, req, res, next). Se ejecutan cuando llamas next(err) o cuando ocurre un error en un middleware async (si lo capturas y lo pasas a next).

// middleware/errorHandler.jsfunction errorHandler(err, req, res, next) {  console.error('Error:', err);  res.status(500).json({ error: 'INTERNAL_ERROR' });}module.exports = { errorHandler };

Debe registrarse al final (después de rutas):

const { errorHandler } = require('./middleware/errorHandler');app.use(errorHandler);

Ejemplo integrado: orden recomendado de registro

Este ejemplo muestra un orden típico: parseo, logging, headers, rutas públicas, routers protegidos, y al final el manejador de errores.

const express = require('express');const { logRequest } = require('./middleware/logRequest');const { setCommonHeaders } = require('./middleware/headers');const { requireBodyFields } = require('./middleware/validate');const { requireApiKey } = require('./middleware/auth');const { errorHandler } = require('./middleware/errorHandler');const app = express();app.use(express.json());app.use(logRequest);app.use(setCommonHeaders);app.get('/health', (req, res) => res.json({ ok: true }));app.post('/contact', requireBodyFields(['email', 'message']), (req, res) => {  res.status(201).json({ ok: true });});app.get('/private', requireApiKey(process.env.API_KEY || 'dev-secret'), (req, res) => {  res.json({ ok: true, auth: req.auth });});app.use(errorHandler);module.exports = { app };

Si notas comportamientos inesperados (por ejemplo, una ruta no se ejecuta), revisa primero el orden y si algún middleware está terminando la respuesta sin llamar a next().

Ahora responde el ejercicio sobre el contenido:

En una app de Express, si un middleware envía una respuesta con res.json(...) o res.send(...), ¿qué práctica es la más adecuada respecto a next()?

¡Tienes razón! Felicitaciones, ahora pasa a la página siguiente.

¡Tú error! Inténtalo de nuevo.

Cuando un middleware termina la respuesta (por ejemplo con res.json o res.send), lo habitual es no llamar a next() después, porque no se espera que continúe la cadena de middlewares.

Siguiente capítulo

Manejo de errores y respuestas consistentes en Node.js con Express

Arrow Right Icon
Portada de libro electrónico gratuitaNode.js para principiantes: crea un backend simple con Express
50%

Node.js para principiantes: crea un backend simple con Express

Nuevo curso

12 páginas

Descarga la aplicación para obtener una certificación gratuita y escuchar cursos en segundo plano, incluso con la pantalla apagada.