wandres.dev
MIDDLEWARE · onRequest y locals

sequence(): encadenar varios middlewares

Cuando un solo onRequest acumula demasiadas responsabilidades, se parte en piezas pequeñas y enfocadas que sequence combina en uno solo. Cómo se importa sequence de astro:middleware, cómo cada eslabón recibe context y next, por qué el orden de la petición sigue el modelo de la cebolla —ida en el orden declarado, vuelta al revés— y cómo los locals que escribe uno los leen los siguientes.

⏱ 17 min

Un middleware que empieza limpio se corrompe con el uso: primero valida la sesión, luego resuelve el idioma, después añade cabeceras de seguridad y de paso mide tiempos. En pocas semanas tu onRequest es un amasijo de asuntos sin relación, imposible de leer y peligroso de tocar. La respuesta de Astro es la misma que da la buena ingeniería a cualquier función que crece de más: partirla en piezas pequeñas, cada una con un solo cometido, y componerlas. sequence es el combinador que las encadena en un único onRequest, y entenderlo bien —sobre todo el orden en que corren a la ida y a la vuelta— es dominar la arquitectura del borde de tu sitio.

🎯 Al terminar esta lección sabrás
  • Reconocer cuándo un onRequest sobrecargado pide partirse en piezas.
  • Combinar varios middlewares en uno solo con sequence.
  • Predecir el orden de ejecución con el modelo de la cebolla.
  • Compartir datos entre eslabones a través de context.locals.

Un onRequest que lo hace todo se vuelve inmanejable

Nada te impide meter toda la lógica transversal en una sola función. Al principio parece cómodo: un archivo, un onRequest, todo a la vista. Pero cada asunto que añades —autenticación, idioma, registro, seguridad— tiene su propia condición, sus propias ramas y su propio momento, y mezclarlos en un cuerpo único los enreda hasta que cambiar uno arriesga romper otro. La señal de alarma es clásica: una función que hace y esto y aquello y lo de más allá está pidiendo a gritos que la separes.

La cura es tratar cada preocupación como un middleware independiente, en su propio archivo, con su nombre y su único cometido. Uno se ocupa de la sesión y nada más; otro, del idioma y nada más; otro, del registro. Cada uno es pequeño, legible y comprobable por separado. Lo que antes era un párrafo confuso se vuelve un puñado de funciones enfocadas, y el archivo src/middleware.ts queda para una sola cosa: decir en qué orden se aplican.

sequence: varios middlewares en fila

La función sequence, importada de astro:middleware, toma varios middlewares y devuelve uno solo que los ejecuta en fila. Ese resultado es lo que exportas como onRequest. Cada eslabón se escribe como el middleware de siempre —recibe context y next, devuelve una Response—; lo que cambia es que su next ya no dispara directamente el renderizado, sino que cede el turno al siguiente middleware de la secuencia.

// src/middleware.ts
import { sequence } from 'astro:middleware';
import { logging } from './middleware/logging';
import { auth } from './middleware/auth';
import { i18n } from './middleware/i18n';

export const onRequest = sequence(logging, auth, i18n);

Cada pieza vive en su archivo y se ocupa de lo suyo. Fíjate en que ninguna sabe de las otras: auth no conoce a i18n, solo llama a next para ceder el paso a quienquiera que venga después. Esa ignorancia mutua es la que permite reordenarlas, quitar una o añadir otra sin tocar su código, cambiando únicamente la línea de sequence.

// src/middleware/auth.ts
import { defineMiddleware } from 'astro:middleware';

export const auth = defineMiddleware(async (context, next) => {
  const sid = context.cookies.get('sid')?.value;
  context.locals.usuario = await buscarSesion(sid);
  return next();
});

El context es el mismo objeto para toda la secuencia, así que es también el canal por el que los eslabones se hablan. Si auth deposita el usuario en context.locals, cualquier middleware posterior —y la página al final— lo encontrará ya resuelto. Por eso el orden no es solo estético: un eslabón que depende de un dato debe ir después del que lo produce.

Mira cómo un segundo eslabón aprovecha lo que dejó el primero. Este i18n lee el usuario que auth ya resolvió para elegir su idioma preferido, y publica el resultado en locals para las páginas:

// src/middleware/i18n.ts
import { defineMiddleware } from 'astro:middleware';

export const i18n = defineMiddleware((context, next) => {
  const preferido = context.locals.usuario?.idioma;
  context.locals.idioma = preferido ?? 'es';
  return next();
});

Que esto funcione depende por completo del orden: si i18n corriera antes que auth, leería un context.locals.usuario todavía vacío y elegiría siempre el idioma por defecto. La secuencia no es una lista cualquiera; es una cadena de dependencias donde cada eslabón presupone lo que los anteriores ya dejaron hecho.

El orden importa: la cebolla de la petición

Aquí está la parte que más confunde y que hay que grabar. Cuando sequence encadena tres middlewares, la petición los recorre como las capas de una cebolla: entra atravesándolas en el orden declarado, llega al centro donde se renderiza la ruta, y sale atravesándolas de nuevo pero en orden inverso. El código que pusiste antes de next corre a la ida, en el orden de la secuencia; el que pusiste después de next corre a la vuelta, del último al primero.

// src/middleware/logging.ts
import { defineMiddleware } from 'astro:middleware';

export const logging = defineMiddleware(async (context, next) => {
  const inicio = Date.now();       // ida: primero de todos
  const response = await next();   // deja correr al resto y al render
  const ms = Date.now() - inicio;  // vuelta: ultimo de todos
  console.log(context.url.pathname, `${ms}ms`);
  return response;
});

Colocado como primer eslabón, logging es el primero en empezar y el último en terminar: su await next() no vuelve hasta que auth, i18n y el renderizado entero han pasado. Por eso envuelve a todos los demás, y por eso mide el tiempo real de la petición completa. Esa es la lógica de la cebolla: el primero de la fila es el que más afuera está, el que abraza a todo lo que viene detrás. Entenderlo te deja decidir con criterio dónde poner cada pieza: lo que debe vigilar todo va primero; lo que depende de un dato va después de quien lo produce.

Reordenar es rediseñar: cómo elegir el orden

Si el orden de una sequence es su arquitectura, conviene tener heurísticas para fijarlo con criterio, y tres bastan para casi todos los casos. La primera: lo que envuelve, va primero. Un middleware que mide o registra la petición entera —como logging— debe ir al principio, porque solo desde la capa más externa abraza todo lo que ocurre dentro, el render incluido.

La segunda: quien produce, antes que quien consume. Si i18n necesita el usuario para elegir idioma, auth —que lo resuelve y lo deja en locals— tiene que ir antes. Las dependencias de datos entre eslabones dictan su orden con la misma fuerza con la que las dependencias entre módulos dictan el suyo.

La tercera: quien puede vetar, cuanto antes. Un guardián que corta la cadena ahorra todo el trabajo posterior si actúa pronto; ponerlo tarde significa preparar datos que un veto tirará a la basura. Con estas tres reglas —envolver fuera, producir antes de consumir, vetar temprano— el orden deja de ser una corazonada y pasa a ser una consecuencia de las relaciones reales entre tus piezas.

flowchart TD
RQ[peticion] --> L1[logging ida]
L1 --> A1[auth ida]
A1 --> I1[i18n ida]
I1 --> R[render de la ruta]
R --> I2[i18n vuelta]
I2 --> A2[auth vuelta]
A2 --> L2[logging vuelta]
L2 --> OUT[respuesta final]
style RQ fill:#89b4fa,color:#11111b
style R fill:#f9e2af,color:#11111b
style OUT fill:#a6e3a1,color:#11111b
📝
Un eslabón que corta detiene a los que le siguen

El modelo de la cebolla tiene una consecuencia práctica sobre los cortes. Si un middleware decide no llamar a next —porque redirige al login o responde un 403—, los eslabones que venían después de él no llegan a ejecutarse, ni a la ida ni a la vuelta, y la ruta no se renderiza. Esto suele ser justo lo que quieres: no tiene sentido resolver el idioma de una página que el guardia de sesión ya vetó. Pero exige colocar los cortes con cabeza: pon primero lo que puede rechazar la petición, para no gastar trabajo en preparativos que un veto posterior tirará a la basura.

ℹ️
sequence no cambia la firma de cada pieza

Un alivio conceptual: cada middleware dentro de una secuencia es exactamente el mismo tipo de función que un onRequest solitario. Recibe context y next, devuelve una Response, se envuelve en defineMiddleware para tipar. No hay una API especial para los eslabones. Lo único que hace sequence es cablear el next de cada uno para que apunte al siguiente en vez de al render directo. Eso significa que puedes escribir y probar un middleware de forma aislada y luego enchufarlo a la secuencia sin cambiar una línea de su cuerpo.

💡
Prueba cada eslabón por separado

Una ganancia silenciosa de partir el middleware en piezas es que cada una se vuelve comprobable de forma aislada. Como un eslabón es una función normal que recibe context y next y devuelve una Response, puedes invocarlo en un test con un context fingido y un next de mentira, y verificar que hace lo suyo —redirigir, rellenar locals, tocar una cabecera— sin levantar el sitio entero. Lo que era un onRequest monolítico e inabordable se vuelve un puñado de funciones que se prueban de una en una.

🧩

Uno por asunto

Cada middleware una sola responsabilidad: sesion, idioma, registro, seguridad. Pequeno y comprobable.

🔗

sequence combina

Toma varias piezas y devuelve un unico onRequest. Reordenar es cambiar una linea, no tocar codigo.

🧅

La cebolla

Ida en el orden declarado, vuelta al reves. El primero de la fila envuelve a todos los demas.

📤

locals compartido

El context es el mismo para toda la secuencia: lo que un eslabon escribe, los siguientes lo leen.

sequence es composición de funciones, y el orden es la arquitectura

Lo que sequence te enseña, si lo miras de cerca, trasciende a Astro: es la composición de funciones aplicada al procesamiento de una petición. Cada middleware es una transformación pequeña que envuelve a la siguiente, y sequence no es más que el operador que las compone en una sola. Es la misma idea que compone dos funciones matemáticas en una tercera, la misma que apila los filtros de una tubería de datos, la misma que anida los envoltorios de una función decorada. Y como toda composición, hereda dos virtudes que aquí valen oro. La primera es que las partes no necesitan conocerse: auth ignora que existe i18n, se limita a llamar a next y confiar en que alguien seguirá la cadena; esa ignorancia mutua es lo que te deja añadir, quitar y reordenar piezas cambiando una sola línea, porque ninguna está soldada a otra. La segunda, más profunda, es que el next de cada eslabón es una continuación: representa todo el resto del cómputo que falta por hacer, empaquetado en una función que decides cuándo invocar. Llamarlo pronto, tarde, o no llamarlo, es controlar el futuro de la petición desde dentro de una pieza que no sabe nada de él. De ahí nace el modelo de la cebolla, que no es una rareza sino la forma inevitable de cualquier sistema donde cada capa envuelve a la siguiente: se entra hacia el centro capa a capa y se sale deshaciendo el camino, y por eso el primero en empezar es el último en terminar. Interiorizar esto cambia cómo diseñas el borde de tu aplicación. Dejas de preguntarte qué debe hacer mi middleware y empiezas a preguntarte qué asuntos hay, en qué orden dependen unos de otros, y quién debe envolver a quién. El orden de una sequence deja de ser un detalle de implementación y se revela como lo que de verdad es: la arquitectura de tu sitio escrita en una sola línea, donde cada posición codifica una decisión sobre qué se comprueba antes, qué se prepara para quién, y qué abraza a todo lo demás.

⚔️ Compón el borde de tu sitio
  1. Parte tu middleware en tres archivos —logging, auth, i18n— cada uno con una única responsabilidad, y combínalos con sequence en src/middleware.ts.
  2. Pon un console.log a la ida y otro a la vuelta en cada eslabón y observa cómo la ida sigue el orden declarado y la vuelta lo invierte.
  3. Haz que auth escriba context.locals.usuario y que i18n, colocado después, lo lea: confirma que un eslabón ve lo que escribió el anterior.
  4. Haz que auth redirija al login cuando no haya sesión y comprueba que i18n y el render ya no se ejecutan para esa petición.