Qué es el middleware en Astro
El punto único por el que pasa toda petición antes de que se pinte una página. Qué es el archivo src/middleware.ts, cómo se declara onRequest con defineMiddleware, qué significan sus dos argumentos context y next, y cómo se reconstruye el ciclo completo de la petición: entrar, decidir, delegar en next, recibir la respuesta y devolverla.
src/middleware.ts: un archivo que Astro busca solo
Astro reserva un nombre. Si colocas un archivo src/middleware.ts —o src/middleware.js— y exportas desde él una función llamada onRequest, Astro la encuentra sin que la registres en ninguna parte y la ejecuta ante cada petición que atiende tu sitio. No hay import que hacer, no hay lista de plugins donde apuntarla: el nombre del archivo y el de la exportación son el contrato. Esa convención es deliberada, y anuncia una intención: que haya un único lugar, sabido de antemano, para la lógica que atraviesa todo.
// src/middleware.tsimport { defineMiddleware } from 'astro:middleware';export const onRequest = defineMiddleware((context, next) => { return next();});Ese es el middleware más pequeño que existe y que a la vez no hace nada: recibe la petición y la deja pasar tal cual llamando a next. Ya funciona; ya intercepta cada ruta. La defineMiddleware que lo envuelve no altera el comportamiento, solo aporta los tipos: gracias a ella, context y next llegan tipados sin que tú anotes nada. Es un ayudante de ergonomía, no un requisito —también puedes tipar la función a mano con MiddlewareHandler—, pero es la forma idiomática y la que usaremos.
Si prefieres ver qué hay debajo de esa comodidad, la forma manual es igual de válida: importas el tipo MiddlewareHandler y anotas con él tu constante, sin envoltorio ninguno.
// src/middleware.tsimport type { MiddlewareHandler } from 'astro';export const onRequest: MiddlewareHandler = (context, next) => { return next();};Las dos formas producen exactamente el mismo comportamiento; defineMiddleware solo te ahorra escribir el tipo a mano. Sea cual sea la que elijas, lo que Astro ejecuta es la exportación onRequest, y ese es el único contrato que de verdad importa.
Conviene fijar desde el principio el alcance. El middleware corre para cada ruta que Astro renderiza: una página .astro, un endpoint, una ruta estática horneada en el build o una servida bajo demanda. Para las páginas prerenderizadas se ejecuta en el momento del build; para las de servidor, en cada petición real. Es, por tanto, el gancho más transversal del framework: no hay forma de renderizar una ruta sin pasar por él.
onRequest: context y next, los dos argumentos
La función recibe siempre dos cosas, y entenderlas es entender el middleware entero. El primero, context, es el contexto de la petición: el mismo objeto que una página conoce como Astro. Trae la request entrante, la url ya parseada, las cookies, los params de la ruta, redirect, rewrite, locals y el resto del kit. Todo lo que una página sabe de su petición, el middleware lo sabe primero, porque corre antes.
export const onRequest = defineMiddleware((context, next) => { console.log('entra:', context.request.method, context.url.pathname); return next();});Ese context es el mismo objeto que una página conoce como Astro, con las mismas piezas a mano: context.request, la petición web estándar; context.url, su dirección ya parseada; context.cookies, para leer y escribir cookies; context.params, los segmentos dinámicos de la ruta; context.clientAddress, la dirección del visitante; y context.locals, el maletín que llenaremos en el próximo capítulo. Que el middleware reciba todo esto antes que la página es justo lo que le deja decidir por adelantado, con la petición entera delante y ninguna línea de la ruta ejecutada aún.
El segundo argumento, next, es una función, y es la pieza que da al middleware su forma peculiar. Llamar a next significa sigue con la cadena: renderiza la ruta que toca —o pasa el turno al siguiente middleware, como veremos— y devuelve su resultado. Ese resultado es una Response, la respuesta HTTP ya construida, envuelta en una promesa; su tipo es Promise<Response>. Por eso el patrón completo suele ser asíncrono: esperas a next, recibes la Response, y decides qué hacer con ella antes de devolverla.
export const onRequest = defineMiddleware(async (context, next) => { const response = await next(); return response;});La obligación es simple y estricta: el middleware debe devolver una Response. Lo normal es devolver la que produce next, quizá retocada; pero también puedes construir una tuya y no llamar a next en absoluto, cortando la cadena antes de que nada se renderice. Olvidar el return es el error de novato: sin él, Astro se queda sin respuesta que emitir.
El ciclo de la petición: antes, next, después
La posición de next dentro de tu función parte el código en dos mitades de significado opuesto, y ahí está la idea que hay que interiorizar. Lo que escribes antes de llamar a next corre en el camino de ida, cuando la petición acaba de entrar y aún no se ha renderizado nada: es el momento de inspeccionar la URL, leer una cookie, comprobar una sesión, preparar datos. Lo que escribes después de next corre en el camino de vuelta, cuando la ruta ya se renderizó y tienes la Response en la mano: es el momento de tocar cabeceras, medir cuánto tardó, registrar el resultado.
export const onRequest = defineMiddleware(async (context, next) => { const inicio = Date.now(); // ida: antes de renderizar const response = await next(); // se renderiza la ruta const ms = Date.now() - inicio; // vuelta: ya hay respuesta console.log(`${context.url.pathname} tardo ${ms}ms`); return response;});Ese pequeño cronómetro captura la esencia: una misma función toca la petición dos veces, una a la ida y otra a la vuelta, con next como la bisagra que separa ambos instantes. La petición entra, tu código de ida decide, next delega el renderizado en Astro, y tu código de vuelta remata sobre la respuesta antes de devolverla al navegador. Ese viaje alrededor de next es el ciclo de la petición, y es el mapa mental con el que se lee todo lo que sigue en este nivel.
Cuándo corre: en el build o en la petición
Una precisión que ahorra malentendidos: el momento en que tu middleware se ejecuta depende del modo de la ruta que atiende. Para una página prerenderizada —horneada en el build—, onRequest corre una sola vez, durante la construcción del sitio, cuando esa página se genera. Para una página servida bajo demanda, corre de veras en cada petición, con el visitante al otro lado. La misma función, dos momentos distintos según a quién sirva.
Esa distinción trae una consecuencia que conviene tener presente. En una ruta estática no hay un visitante real cuando el middleware corre: leer context.request.headers o una cookie en ese contexto no significa nada, porque no hay petición viva de nadie, solo el build fabricando ficheros. Las decisiones que dependen de quién pide —su sesión, su idioma, su dirección— solo cobran sentido en rutas bajo demanda, las únicas donde existe un visitante concreto al que responder.
La regla mental es limpia: el middleware siempre corre, pero cuándo y con qué información depende del modo de la ruta. En el build, para las estáticas, sin nadie al otro lado; en cada petición, para las dinámicas, con todo el contexto real de la visita. Diseña tus comprobaciones sabiendo en cuál de esos dos mundos viven las rutas que van a tocar.