Express.js
Middleware: composición y responsabilidades
Diseño, composición, alcance, orden y responsabilidades de middleware en Express.js 5.
- Última actualización
- Actualizada
- Nivel
- Fundamentos
Express.js
Diseño, composición, alcance, orden y responsabilidades de middleware en Express.js 5.
Un middleware de Express recibe request, response y next. Puede observar o modificar contexto, responder, delegar o propagar un error.
import type { RequestHandler } from 'express';
export const requestId: RequestHandler = (request, response, next) => {
const requestId = request.get('x-request-id') ?? crypto.randomUUID();
response.locals.requestId = requestId;
response.set('X-Request-Id', requestId);
next();
};La función no es valiosa por llamarse middleware. Es valiosa porque la asignación de request ID es transversal, reutilizable y necesita participar en el ciclo HTTP.
Sin middleware, cada handler repetiría autenticación, parsing, logging, límites y manejo de contexto. El pipeline permite componer esas responsabilidades.
Sin disciplina, el mismo mecanismo produce:
request o res.locals.middleware = filtro o etapa HTTP
entrada
→ request + response + contexto actual
salida posible
→ next()
→ response terminal
→ next(error)Una función normal es preferible cuando no necesita controlar el pipeline. Un caso de uso no debería recibir next ni depender de Express.
(request, response, next) => void | Promise<void>(error, request, response, next) => void | Promise<void>Los cuatro parámetros son parte de la identificación del error middleware. No elimines next de la firma solo porque el cuerpo no lo usa.
Registrado con app.use o métodos de la app. Puede cubrir todo el servicio o un prefijo.
app.use(accessLogger);
app.use('/api', apiRateLimiter);Pertenece a un módulo y solo corre cuando el router montado coincide.
ordersRouter.use(requireAuthentication);Protege una operación concreta.
router.post('/', authorize('orders:create'), validate(schema), handler);Responde y no delega, como un 404 o una respuesta desde cache.
Recibe fallos propagados y decide si mapear, registrar o delegar.
Express incluye:
express.json()express.urlencoded()express.text()express.raw()express.static()Cada uno representa decisiones. Por ejemplo, express.json() consume el stream, interpreta bytes, limita tamaño y puede producir errores antes de la ruta.
app.use(express.json({
limit: '100kb',
strict: true,
}));No habilites parsers globales con límites enormes “por si acaso”.
Antes de instalar un paquete analiza:
Helmet, CORS o rate limiting no son botones de seguridad. Son controles parciales.
router.post(
'/',
authenticate,
authorize('orders:create'),
validate(createOrderSchema),
createOrderHandler,
);El orden expresa dependencias:
Cuando el orden es crítico, documéntalo y pruébalo.
Una factory permite parametrizar comportamiento:
function authorize(permission: Permission): RequestHandler {
return (request, response, next) => {
const actor = response.locals.actor;
if (!actor.permissions.includes(permission)) {
response.status(403).json({ code: 'FORBIDDEN' });
return;
}
next();
};
}La factory se configura al construir la app; el middleware resultante se ejecuta por request.
En Express 5, una Promise retornada que se rechaza entra al error pipeline:
const loadOrder: RequestHandler = async (request, response, next) => {
const order = await orders.findById(request.params.orderId);
response.locals.order = order;
next();
};Esto no cubre:
Esos errores deben conectarse explícitamente o trasladarse a una cola.
Adecuado para contexto interno de la response actual:
response.locals.actor = actor;
response.locals.validatedBody = input;Puede ser útil para contratos ampliamente usados, pero requiere declaration merging y disciplina.
Permite acceder a request ID o tracing sin pasar parámetros por cada función. Es útil para observabilidad, pero no debe ocultar dependencias de negocio.
Usa middleware cuando la función necesita:
Usa una función normal cuando:
Incorrecto:
async function checkStockMiddleware(request, response, next) {
// reglas, queries, reserva, email...
}Mejor:
const result = await reserveStock.execute(input);export function makeAuthenticate(tokens: TokenVerifier): RequestHandler {
return async (request, response, next) => {
const authorization = request.get('authorization');
if (!authorization?.startsWith('Bearer ')) {
response.status(401).json({ code: 'AUTHENTICATION_REQUIRED' });
return;
}
const token = authorization.slice('Bearer '.length);
const actor = await tokens.verify(token);
response.locals.actor = actor;
next();
};
}Flujo:
Puede duplicar logs, parsing o rate limits. Revisa composición de routers.
Expone la request a capas posteriores y puede causar doble response.
Un middleware anterior no corrió o el orden cambió. Evita non-null assertions sin garantías.
El error handler no puede reemplazar la salida completa; debe delegar o cerrar.
Una verificación innecesaria en health checks o assets aumenta latencia. Limita el alcance.
Reutilización no implica pipeline. Muchas funciones deben permanecer puras.
Oculta demasiadas decisiones y dificulta pruebas.
Los siguientes handlers dependen de propiedades que el compilador no conoce.
Aumenta superficie de vulnerabilidad por una tarea que quizá podía resolverse con pocas líneas.
Un logger que registra justo antes de next() no conoce status final ni duración completa.
Prueba cada decisión:
Los middleware puros pueden probarse con objetos mínimos, pero una prueba HTTP de integración detecta mejor interacción real con Express.
Más middleware puede mejorar composición, pero también:
No optimices por cantidad de archivos. Optimiza por claridad del flujo.
res.locals almacena contexto por request, no estado global.authorize debe ejecutarse después de authenticate?AsyncLocalStorage no debería ocultar el actor del negocio?Routing y matching de rutas explica cómo Express decide qué pipeline ejecutar para cada combinación de método y path.