Qué es un middleware y para qué sirve en el desarrollo backend

D
DanisCh
(Actualizado: ) 12 min de lectura
Qué es un middleware y para qué sirve en el desarrollo backend
DevOps Programación

Si llevas un tiempo trabajando con frameworks backend como Express, Django, FastAPI o Laravel, habrás visto el término middleware en la documentación. Es uno de esos conceptos que suena más complejo de lo que es. Una vez que lo entiendes, lo verás en todas partes y empezarás a usarlo para resolver problemas de forma más elegante.

En esta guía aprenderás qué es un middleware, cómo funciona, para qué se usa en la práctica y cómo escribir los tuyos propios.

Qué es un middleware

Un middleware es una función que se ejecuta entre que llega una petición HTTP y el momento en que el handler final la procesa. Puede leer y modificar la petición, generar una respuesta anticipada (cortocircuitando el flujo) o simplemente pasar el control al siguiente middleware.

La palabra viene de "software intermedio": código que vive en el medio entre el cliente y la lógica de negocio.

La mejor forma de entenderlo es visual. Sin middleware:

# Petición → Handler → Respuesta
# El handler hace todo: autenticar, validar, procesar, responder

Con middleware:

# Petición
#   → Middleware 1 (logging)
#   → Middleware 2 (autenticación)
#   → Middleware 3 (validación)
#   → Handler (lógica de negocio)
#   → Respuesta
#
# Cada middleware decide si pasa el control al siguiente
# o corta el flujo devolviendo una respuesta directamente

La clave es que cada middleware tiene una responsabilidad única. El resultado es código más limpio, más reutilizable y mucho más fácil de mantener.

Cómo funciona en Express (Node.js)

Express es el framework donde el concepto de middleware es más explícito y visible. Un middleware en Express es una función que recibe tres argumentos: req (la petición), res (la respuesta) y next (función para pasar al siguiente middleware).

// La firma de un middleware en Express
function miMiddleware(req, res, next) {
 // 1. Hacer algo con la petición (leer cabeceras, validar, loggear...)
 // 2. Opción A: pasar al siguiente middleware
 next();
 // 2. Opción B: cortar el flujo y responder directamente
 // res.status(401).json({ error: 'No autorizado' });
}
// Middleware global: se ejecuta para TODAS las peticiones
app.use(miMiddleware);
// Middleware para una ruta específica
app.get('/perfil', verificarToken, (req, res) => {
 res.json({ usuario: req.usuario });
});
// Varios middlewares en secuencia para una ruta
app.post('/articulos',
 verificarToken,        // primero: ¿está autenticado?
 verificarRolEditor,    // segundo: ¿tiene permisos?
 validarArticulo,       // tercero: ¿los datos son correctos?
 crearArticulo          // cuarto: lógica de negocio
);

El flujo de ejecución

 // Ejemplo detallado para ver el orden de ejecución
const express = require('express');
const app = express();
// Middleware 1: logging (global)
app.use((req, res, next) => {
 console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`);
 next();   // pasar al siguiente
});
// Middleware 2: añadir cabeceras CORS (global)
app.use((req, res, next) => {
 res.setHeader('Access-Control-Allow-Origin', '*');
 next();
});
// Handler de la ruta
app.get('/hola', (req, res) => {
 res.json({ mensaje: 'Hola mundo' });
});
// Cuando llega GET /hola:
// 1. Middleware de logging → imprime la petición → next()
// 2. Middleware de CORS → añade cabecera → next()
// 3. Handler → responde con JSON
// Cortocircuitar el flujo: no llamar a next()
function verificarToken(req, res, next) {
 const token = req.headers.authorization?.replace('Bearer ', '');
 if (!token) {
   // No llamamos a next(): el flujo termina aquí
   return res.status(401).json({ error: 'Token requerido' });
 }
 try {
   const usuario = jwt.verify(token, process.env.JWT_SECRET);
   req.usuario = usuario;   // añadir datos al objeto req para el handler
   next();                  // token válido: pasar al siguiente
 } catch {
   return res.status(401).json({ error: 'Token inválido o expirado' });
 }
}
// Handler que usa los datos añadidos por el middleware
app.get('/perfil', verificarToken, (req, res) => {
 // req.usuario fue añadido por verificarToken
 res.json({ id: req.usuario.sub, email: req.usuario.email });
});

Los middlewares más habituales

Logging: registrar todas las peticiones

// Middleware de logging manual
function logging(req, res, next) {
 const inicio = Date.now();
 // Interceptar cuando la respuesta termina
 res.on('finish', () => {
   const duracion = Date.now() - inicio;
   const nivel = res.statusCode >= 500 ? 'ERROR'
               : res.statusCode >= 400 ? 'WARN'
               : 'INFO';
   console.log(
     `[${nivel}] ${req.method} ${req.path} → ${res.statusCode} (${duracion}ms)`
   );
 });
 next();
}
// O usar morgan, la librería estándar para logging en Express:
const morgan = require('morgan');
app.use(morgan('combined'));   // formato Apache combinado
app.use(morgan('dev'));        // formato conciso para desarrollo
// [GET] /usuarios 200 23ms

Parseo del cuerpo de la petición

// Sin este middleware, req.body sería undefined
app.use(express.json());          // parsear application/json
app.use(express.urlencoded({ extended: true })); // parsear formularios HTML
// Después de este middleware, todos los handlers tienen acceso a req.body:
app.post('/usuarios', (req, res) => {
 const { nombre, email } = req.body;   // disponible gracias al middleware
 // ...
});

Autenticación

 // Middleware de autenticación JWT reutilizable
function autenticar(req, res, next) {
 const authHeader = req.headers.authorization;
 if (!authHeader?.startsWith('Bearer ')) {
   return res.status(401).json({
     error: { codigo: 'NO_AUTENTICADO', mensaje: 'Token requerido' }
   });
 }
 const token = authHeader.slice(7);
 try {
   req.usuario = jwt.verify(token, process.env.JWT_SECRET);
   next();
 } catch (error) {
   return res.status(401).json({
     error: {
       codigo: 'TOKEN_INVALIDO',
       mensaje: error.name === 'TokenExpiredError'
         ? 'El token ha expirado'
         : 'Token inválido'
     }
   });
 }
}
// Autorización: verificar rol (depende de autenticar)
function requiereRol(...roles) {
 return (req, res, next) => {
   if (!roles.includes(req.usuario?.rol)) {
     return res.status(403).json({
       error: { codigo: 'SIN_PERMISOS', mensaje: 'Acceso denegado' }
     });
   }
   next();
 };
}
// Combinar autenticación y autorización
app.delete('/usuarios/:id',
 autenticar,
 requiereRol('admin'),
 eliminarUsuario
);

Validación de datos

// Middleware de validación con Zod
const { z } = require('zod');
function validar(schema) {
 return (req, res, next) => {
   const resultado = schema.safeParse(req.body);
   if (!resultado.success) {
     return res.status(422).json({
       error: {
         codigo: 'VALIDACION_FALLIDA',
         mensaje: 'Los datos enviados no son válidos',
         detalles: resultado.error.errors.map(e => ({
           campo:   e.path.join('.'),
           mensaje: e.message
         }))
       }
     });
   }
   req.body = resultado.data;   // datos validados y transformados
   next();
 };
}
// Definir schemas de validación
const schemaUsuario = z.object({
 nombre:     z.string().min(2).max(100),
 email:      z.string().email(),
 edad:       z.number().int().min(0).max(150).optional(),
 contraseña: z.string().min(8),
});
// Usar el middleware de validación
app.post('/usuarios',
 validar(schemaUsuario),   // valida antes de llegar al handler
 crearUsuario
);

Rate limiting: limitar el número de peticiones

const rateLimit = require('express-rate-limit');
// Limitar peticiones globalmente
const limitadorGlobal = rateLimit({
 windowMs: 15 * 60 * 1000,   // ventana de 15 minutos
 max:      100,               // máximo 100 peticiones por ventana por IP
 message:  { error: { codigo: 'DEMASIADAS_PETICIONES', mensaje: 'Límite excedido' } },
 standardHeaders: true,       // incluir cabeceras X-RateLimit-*
});
// Limitar solo el endpoint de login (más estricto para prevenir fuerza bruta)
const limitadorLogin = rateLimit({
 windowMs: 15 * 60 * 1000,
 max:      5,   // solo 5 intentos de login por ventana
 skipSuccessfulRequests: true,  // no contar los logins exitosos
});
app.use(limitadorGlobal);
app.post('/auth/login', limitadorLogin, manejarLogin);

Manejo centralizado de errores

// En Express, el middleware de errores tiene 4 parámetros (incluyendo err)
// DEBE ir después de todas las rutas y middlewares normales
function manejarErrores(err, req, res, next) {
 // Loggear el error con detalles (en producción, a un servicio como Sentry)
 console.error({
   mensaje:  err.message,
   stack:    err.stack,
   url:      req.url,
   método:   req.method,
   usuario:  req.usuario?.sub
 });
 // Determinar el código de estado
 const status = err.status || err.statusCode || 500;
 // En producción, no revelar el stack trace
 const respuesta = {
   error: {
     codigo:  err.codigo || 'ERROR_INTERNO',
     mensaje: status < 500
       ? err.message
       : 'Ocurrió un error interno. Inténtalo de nuevo más tarde.'
   }
 };
 res.status(status).json(respuesta);
}
// Registrarlo al final
app.use(manejarErrores);
// Cómo lanzar errores desde los handlers para que lleguen aquí:
app.get('/usuarios/:id', async (req, res, next) => {
 try {
   const usuario = await db.buscarUsuario(req.params.id);
   if (!usuario) {
     const error = new Error('Usuario no encontrado');
     error.status = 404;
     throw error;
   }
   res.json(usuario);
 } catch (err) {
   next(err);   // pasar el error al middleware de errores
 }
});

CORS: permitir peticiones de otros orígenes

const cors = require('cors');
// Permitir todos los orígenes (solo en desarrollo)
app.use(cors());
// Configuración restrictiva para producción
app.use(cors({
 origin: ['https://mi-app.com', 'https://admin.mi-app.com'],
 methods:          ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
 allowedHeaders:   ['Content-Type', 'Authorization'],
 credentials:      true,   // permitir cookies y cabeceras de autenticación
 maxAge:           86400,  // cachear la respuesta preflight 24h
}));

Middleware en FastAPI (Python)

 from fastapi import FastAPI, Request, Response
from starlette.middleware.base import BaseHTTPMiddleware
import time
import logging
app = FastAPI()
# Middleware en FastAPI: clase que hereda de BaseHTTPMiddleware
class LoggingMiddleware(BaseHTTPMiddleware):
   async def dispatch(self, request: Request, call_next):
       inicio = time.time()
       # Código que se ejecuta ANTES del handler
       logging.info(f"→ {request.method} {request.url.path}")
       # Llamar al siguiente middleware o al handler
       respuesta = await call_next(request)
       # Código que se ejecuta DESPUÉS del handler (con la respuesta)
       duracion = round((time.time() - inicio) * 1000)
       logging.info(f"← {respuesta.status_code} ({duracion}ms)")
       # Añadir cabecera personalizada a la respuesta
       respuesta.headers["X-Process-Time"] = str(duracion)
       return respuesta
app.add_middleware(LoggingMiddleware)
from fastapi import FastAPI, Request
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.responses import JSONResponse
class LimitarTamañoCuerpoMiddleware(BaseHTTPMiddleware):
   def __init__(self, app, max_bytes: int = 1_000_000):   # 1MB por defecto
       super().__init__(app)
       self.max_bytes = max_bytes
   async def dispatch(self, request: Request, call_next):
       content_length = request.headers.get("content-length")
       if content_length and int(content_length) > self.max_bytes:
           return JSONResponse(
               status_code=413,
               content={"error": "El cuerpo de la petición supera el límite permitido"}
           )
       return await call_next(request)
app.add_middleware(LimitarTamañoCuerpoMiddleware, max_bytes=5_000_000)  # 5MB
from fastapi import Depends, HTTPException, Header
import jwt
# En FastAPI, la autenticación se hace con dependencias, no middlewares
# Las dependencias son más idiomáticas y permiten documentación automática
def obtener_usuario_actual(authorization: str = Header(...)):
   if not authorization.startswith("Bearer "):
       raise HTTPException(status_code=401, detail="Token requerido")
   token = authorization.replace("Bearer ", "")
   try:
       return jwt.decode(token, SECRET_KEY, algorithms=["HS256"])
   except jwt.ExpiredSignatureError:
       raise HTTPException(status_code=401, detail="Token expirado")
   except jwt.InvalidTokenError:
       raise HTTPException(status_code=401, detail="Token inválido")
def requiere_rol(*roles):
   def verificar(usuario = Depends(obtener_usuario_actual)):
       if usuario["rol"] not in roles:
           raise HTTPException(status_code=403, detail="Sin permisos")
       return usuario
   return verificar
# Uso en rutas
@app.delete("/usuarios/{id}")
def eliminar_usuario(
   id: int,
   usuario = Depends(requiere_rol("admin"))
):
   return db.eliminar_usuario(id)

Middleware en Django (Python)

 # Django tiene su propio sistema de middleware
# Se configuran en settings.py en la lista MIDDLEWARE
MIDDLEWARE = [
   'django.middleware.security.SecurityMiddleware',
   'django.contrib.sessions.middleware.SessionMiddleware',
   'django.middleware.common.CommonMiddleware',
   'django.middleware.csrf.CsrfViewMiddleware',          # protección CSRF
   'django.contrib.auth.middleware.AuthenticationMiddleware',
   'django.contrib.messages.middleware.MessageMiddleware',
   'django.middleware.clickjacking.XFrameOptionsMiddleware',
   'mi_app.middleware.LoggingMiddleware',   # nuestro middleware personalizado
]
# Crear un middleware personalizado en Django
import time
import logging
logger = logging.getLogger(__name__)
class LoggingMiddleware:
   def __init__(self, get_response):
       self.get_response = get_response   # el siguiente middleware o la vista
   def __call__(self, request):
       # Código ANTES de la vista
       inicio = time.time()
       logger.info(f"→ {request.method} {request.path}")
       # Llamar a la vista (o al siguiente middleware)
       response = self.get_response(request)
       # Código DESPUÉS de la vista
       duracion = round((time.time() - inicio) * 1000)
       logger.info(f"← {response.status_code} ({duracion}ms)")
       response["X-Process-Time"] = str(duracion)
       return response
class SoloAPIAutenticadaMiddleware:
   """Requiere autenticación para todos los endpoints /api/"""
   def __init__(self, get_response):
       self.get_response = get_response
   def __call__(self, request):
       if request.path.startswith('/api/') and not request.user.is_authenticated:
           from django.http import JsonResponse
           return JsonResponse(
               {"error": "Autenticación requerida"},
               status=401
           )
       return self.get_response(request)

Composición de middlewares: el pipeline

Los middlewares forman un pipeline (tubería) donde cada uno procesa la petición y la pasa al siguiente. El orden en que los registras importa: se ejecutan en orden al entrar, y en orden inverso al salir.

// Visualización del pipeline
//
// Petición entrante
//      ↓
// [Middleware A: inicio]   ← se ejecuta primero al entrar
//      ↓
// [Middleware B: inicio]
//      ↓
// [Middleware C: inicio]   ← se ejecuta último al entrar
//      ↓
//   [HANDLER]              ← lógica de negocio
//      ↓
// [Middleware C: fin]      ← se ejecuta primero al salir
//      ↓
// [Middleware B: fin]
//      ↓
// [Middleware A: fin]      ← se ejecuta último al salir
//      ↓
// Respuesta al cliente
// En Express, el patrón de "antes y después" con next():
function middlewareConAntesDespues(req, res, next) {
 console.log('ANTES del handler');     // ejecuta al entrar
 next();
 // En Express, el código DESPUÉS de next() no se ejecuta de forma útil
 // para esto usa res.on('finish') o un middleware de errores
}
// En FastAPI/Starlette, el patrón es más explícito:
async def dispatch(self, request, call_next):
   # código ANTES
   respuesta = await call_next(request)   # ejecutar el handler
   # código DESPUÉS (con la respuesta disponible)
   return respuesta

Cuándo usar middleware y cuándo no

El middleware es la herramienta correcta cuando la lógica:

  • Se aplica a muchas rutas (logging, autenticación, CORS, rate limiting).
  • Es transversal: no pertenece a ningún recurso concreto sino a la capa de infraestructura.
  • Necesita ejecutarse antes o después de la lógica de negocio de forma consistente.
  • Es independiente del contexto de negocio: el middleware de logging no sabe si está procesando un pedido o un usuario.

El middleware no es la herramienta correcta cuando la lógica:

  • Solo aplica a una ruta específica: ponla directamente en el handler.
  • Necesita contexto de negocio complejo: eso pertenece a la capa de servicio.
  • Genera efectos secundarios que dependen del resultado del handler: puede ser difícil de gestionar en la capa de middleware.

Resumen

  • Un middleware es una función que se ejecuta entre la petición HTTP y el handler final. Puede leer y modificar la petición y la respuesta, pasar el control al siguiente middleware o cortar el flujo devolviendo una respuesta directamente.
  • Los middlewares forman un pipeline: se ejecutan en el orden en que se registran y, al salir, en orden inverso.
  • En Express, un middleware recibe (req, res, next). Llama a next() para continuar o responde directamente para cortar el flujo. El middleware de errores tiene cuatro parámetros: (err, req, res, next).
  • Los usos más habituales son: logging, parseo del cuerpo, autenticación, autorización por rol, validación de datos, rate limiting, CORS y manejo centralizado de errores.
  • El patrón más limpio es encadenar middlewares específicos por ruta en lugar de hacerlo todo en el handler: app.post('/ruta', autenticar, validar(schema), handler).
  • Usa middleware para lógica transversal que aplica a muchas rutas. Para lógica específica de una sola ruta, ponla directamente en el handler.

¿Te ha gustado esta entrada?

Compártela con tus compañeros para que también sigan aprendiendo.

Comunidad y Comentarios

0 COMENTARIOS

No hay comentarios todavía. Sé el primero en compartir tu opinión.

Escribe tu opinión
Respondiendo a