Express.js
Ciclo de vida de una petición
Recorrido completo de una petición por middleware, routers, handlers, respuestas, errores, aborts y cleanup.
- Última actualización
- Actualizada
- Nivel
- Fundamentos
Express.js
Recorrido completo de una petición por middleware, routers, handlers, respuestas, errores, aborts y cleanup.
Cuando llega una request, Express no busca primero “la ruta correcta” y después ejecuta todo lo demás. Recorre un stack en el orden en que fue registrado. En ese stack conviven middleware global, middleware por path, routers, handlers de ruta, 404 y middleware de error.
El modelo mental es:
request HTTP
↓
application middleware
↓
router middleware
↓
route middleware
↓
handler final
↓
response
si ocurre un error
↓
error middlewareEl orden no es un detalle de organización: forma parte del comportamiento de la aplicación.
Sin un pipeline común, cada ruta tendría que repetir tareas como:
Express permite componer esas etapas y reutilizarlas, pero esa composición también introduce riesgos: dependencias implícitas, ramas que no terminan y respuestas dobles.
Una configuración típica puede verse así:
app.use(requestId);
app.use(accessLogger);
app.use(express.json({ limit: '100kb' }));
app.use('/api/orders', ordersRouter);
app.use(notFoundHandler);
app.use(errorHandler);El flujo para POST /api/orders sería:
requestId añade un identificador.accessLogger inicia medición y registra al terminar.express.json intenta interpretar el body.ordersRouter evalúa sus rutas internas.Una capa solo participa cuando coincide con el contexto para el que fue registrada.
app.use('/api', apiLogger);
app.use('/admin', adminRouter);Una request a /api/orders puede pasar por apiLogger, pero no por adminRouter. Dentro de un router también existe matching por método y path.
Una capa no coincide y Express continúa sin ejecutarla.
La capa llama next() y entrega control a la siguiente capa compatible.
function requireJson(request, response, next) {
if (!request.is('application/json')) {
response.status(415).json({ code: 'UNSUPPORTED_MEDIA_TYPE' });
return;
}
next();
}La capa finaliza o inicia la response mediante json, send, end, redirect, file o stream.
La capa llama next(error) o, en Express 5, retorna una Promise que se rechaza.
next() solo transfiere control. No confirma éxito, no hace commit y no cierra la conexión.
function attachUser(request, response, next) {
response.locals.user = { id: 'user_123' };
next();
}Después de next(), la función puede continuar ejecutándose. Esto permite lógica “alrededor” del siguiente middleware, pero requiere cuidado:
async function measure(request, response, next) {
const startedAt = performance.now();
response.on('finish', () => {
const duration = performance.now() - startedAt;
console.log({ duration, status: response.statusCode });
});
next();
}No debes asumir que next() espera a que toda la cadena termine como una llamada normal.
Un middleware puede detener la cadena respondiendo temprano:
function requireAuthentication(request, response, next) {
if (!response.locals.user) {
response.status(401).json({ code: 'AUTHENTICATION_REQUIRED' });
return;
}
next();
}Esto es correcto cuando la capa tiene autoridad para terminar el flujo. El problema aparece cuando responde y luego delega.
function brokenMiddleware(request, response, next) {
response.status(401).json({ code: 'AUTHENTICATION_REQUIRED' });
next();
}La siguiente capa puede intentar escribir otra respuesta. El resultado habitual es ERR_HTTP_HEADERS_SENT, aunque también puede existir una respuesta parcial o un stream roto.
La corrección es elegir una sola salida:
response.status(401).json({ code: 'AUTHENTICATION_REQUIRED' });
return;Una capa que no responde ni llama next() deja la solicitud esperando:
function brokenMiddleware(_request, _response, _next) {
// No response and no delegation
}El cliente solo saldrá cuando ocurra un timeout o cierre la conexión. Este fallo suele detectarse como requests pendientes, sockets abiertos y ausencia de logs de finalización.
El pipeline normal utiliza funciones con tres argumentos. El pipeline de error utiliza cuatro:
function errorHandler(error, request, response, next) {
if (response.headersSent) {
next(error);
return;
}
response.status(500).json({ code: 'INTERNAL_ERROR' });
}Express salta middleware normal mientras busca una capa de error compatible. Si el error handler llama next() sin error, puede volver al flujo normal restante; normalmente esto no es lo deseado para una API.
app.get('/orders/:id', async (request, response) => {
const order = await orders.findById(request.params.id);
response.json(order);
});Si la Promise retornada se rechaza, Express 5 entra al pipeline de error. Esto no cubre trabajo desprendido:
app.post('/reports', async (_request, response) => {
generateReportLater(); // rejection may escape the request lifecycle
response.sendStatus(202);
});Una tarea importante debe entregarse a una cola persistente o manejarse explícitamente.
Datos como identidad, tenant, request ID o input validado pueden viajar en res.locals, propiedades tipadas o AsyncLocalStorage.
El contexto debe cumplir:
Ejemplo:
response.locals.context = {
requestId,
actor,
tenantId,
};Una response puede:
finish).close).Para logging y cleanup conviene distinguirlos:
function observeLifecycle(request, response, next) {
let completed = false;
response.on('finish', () => {
completed = true;
console.log({ event: 'response_finished', status: response.statusCode });
});
response.on('close', () => {
if (!completed) {
console.warn({ event: 'response_closed_early' });
}
});
next();
}Recursos adquiridos durante la request deben liberarse en éxito, error o cierre:
No todo recurso debe vivir en middleware. Una conexión transaccional, por ejemplo, suele pertenecer al boundary del caso de uso, no a un middleware global.
router.post(
'/',
authenticate,
authorize('orders:create'),
validate(createOrderSchema),
async (request, response) => {
const order = await createOrder.execute({
actor: response.locals.actor,
input: response.locals.validatedBody,
});
response
.status(201)
.location(`/orders/${order.id}`)
.json({ data: order });
},
);Flujo:
authenticate resuelve identidad o responde 401.authorize verifica permiso general o responde 403.validate convierte datos no confiables a un input válido.JSON inválido nunca llega al handler. El error middleware debe reconocer el error de parsing y responder un contrato público.
El orden es incorrecto. Una capa no puede consumir contexto que todavía no existe.
Ya no puedes sustituir la response por JSON. Debes delegar al handler predeterminado o cerrar la conexión de forma segura.
La query puede seguir ejecutándose si no propagas cancelación. El cierre de la response no cancela automáticamente todas las dependencias.
No verá errores de capas registradas después.
Puede causar una respuesta doble.
Oculta entradas, dependencias y errores detrás del pipeline HTTP.
Si un middleware requiere res.locals.actor, esa dependencia debe reflejarse en composición, tipos y pruebas.
Los aborts y errores también deben liberar recursos.
Un test de integración debe verificar:
Ejemplo conceptual:
const calls: string[] = [];
app.use((_req, _res, next) => {
calls.push('A');
next();
});
app.get('/test', (_req, res) => {
calls.push('handler');
res.sendStatus(204);
});Después de la request, calls debe ser ['A', 'handler'].
next() no espera ni envía la response.next() no equivale a await?finish y close?next(error), ocurre un throw síncrono o se rechaza una Promise retornada en Express 5.finish indica que la response fue entregada al sistema; close puede ocurrir antes de completarse.Middleware: composición y responsabilidades profundiza cómo diseñar capas reutilizables sin convertir el pipeline en arquitectura oculta.