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.
Hasta ahora cada página resolvía su propia suerte: leía sus datos, decidía su 404, componía su cabecera. Pero hay lógica que no pertenece a una página sino a todas —comprobar una sesión, medir un tiempo, fijar una cabecera de seguridad—, y repetirla archivo por archivo es una condena. El middleware es la respuesta de Astro a ese problema: una función, una sola, que se ejecuta en el instante en que llega cualquier petición y antes de que se renderice la ruta que le toca. Es el portero del sitio entero, el punto por el que todo pasa a la ida y a la vuelta, el lugar donde una política transversal se escribe una vez y cubre cada dirección sin excepción.
- Situar el middleware como el punto único por el que pasa toda petición.
- Declarar
onRequestensrc/middleware.tscondefineMiddleware. - Distinguir los dos argumentos que recibe:
contextynext. - Reconstruir el ciclo de la petición desde que entra hasta la respuesta.
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.ts
import { 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.ts
import 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.
flowchart TD N[navegador pide una URL] --> MW[onRequest recibe context y next] MW --> A[codigo de ida antes de next] A --> NX[llamada a next] NX --> R[Astro renderiza la ruta] R --> B[codigo de vuelta con la respuesta] B --> OUT[la respuesta vuelve al navegador] style N fill:#89b4fa,color:#11111b style NX fill:#f9e2af,color:#11111b style OUT fill:#a6e3a1,color:#11111b
Puedes escribir el middleware sin defineMiddleware y funcionará igual; lo único que perderías es el tipado automático de context y next. La alternativa manual es anotar la constante con el tipo MiddlewareHandler importado de astro. Elijas la que elijas, el comportamiento es idéntico: lo que Astro ejecuta es la función onRequest que exportas, y el envoltorio solo existe para que tu editor sepa qué hay dentro de context sin que tú lo declares.
No hay registro ni configuración que valga: Astro reconoce el middleware por dos nombres exactos. El archivo debe llamarse middleware y vivir en la raíz de src, no en una subcarpeta; la función debe exportarse como onRequest. Si cambias cualquiera de los dos, el mecanismo deja de dispararse en silencio, sin un solo error que te avise. Por eso, cuando un middleware no parece ejecutarse, lo primero que hay que revisar es justo esto: dónde está el archivo y cómo se llama la exportación.
El contrato del middleware es inflexible en un punto: tiene que devolver una Response. Casi siempre esa respuesta es la que retorna next, así que basta con return next() o con esperar su resultado y devolverlo. Pero si añades una rama —un if que decide algo— y en alguno de sus caminos olvidas el return, Astro se queda sin nada que emitir y la petición se cae. La regla mental es sencilla: todo camino de tu onRequest termina devolviendo una Response, venga de next o construida por ti.
Un solo portero
src/middleware.ts con una exportacion onRequest cubre todas las rutas del sitio sin registrarla en ningun sitio.
La firma
Recibe context y next, y devuelve una Response o una promesa de Response. Nada mas y nada menos.
Antes y despues
El codigo antes de next corre a la ida; el codigo despues, a la vuelta con la respuesta ya hecha.
defineMiddleware
Envuelve la funcion para tipar context y next sin esfuerzo. Es ergonomia, no un requisito.
Para apreciar de veras lo que el middleware ofrece conviene cambiar de metáfora sobre lo que es una petición web. La intuición ingenua la trata como un evento puntual: llega una URL, se dispara una página, sale un HTML. Pero el middleware te invita a verla de otro modo, más fértil: una petición no es un evento, es un valor que fluye por una tubería, y tu sitio no es una colección de páginas aisladas sino una cadena de transformaciones por las que ese valor pasa antes de convertirse en respuesta. onRequest es el primer eslabón de esa tubería, el que recibe el valor crudo y decide qué hacer con él antes de que cualquier página lo vea. Y next es la operación clave: no es un simple continuar, es una inversión de control: tu código llama a la maquinaria del framework y recibe de vuelta el fruto de ella, quedando envuelto alrededor del renderizado como una capa que abraza a otra. Esa forma —código, luego next, luego más código— es exactamente el patrón que en la teoría de la programación se llama envoltura o aspecto: una preocupación transversal que se factoriza fuera de las páginas y se aplica a todas de golpe. Es la misma idea que un decorador que envuelve una función, que un interceptor que rodea una llamada, que un combinador que compone comportamientos sin que las partes lo sepan. La razón profunda de que Astro reserve un único archivo con un único nombre es esa: quiere que exista un lugar canónico donde vive lo que es cierto para toda petición, separado de lo que es cierto para una página concreta. Cuando dejas de ver src/middleware.ts como un archivo más y empiezas a verlo como el borde de tu aplicación —la aduana por la que todo entra y sale—, dejas de repartir lógica transversal por las páginas y empiezas a colocarla donde de verdad pertenece: en el punto por el que, quieras o no, absolutamente todo pasa.
- Crea
src/middleware.tscon unonRequestenvuelto endefineMiddlewareque se limite areturn next()y comprueba que el sitio sigue funcionando igual. - Añade un
console.logantes denexty otro después: observa en la terminal que el primero sale a la ida y el segundo a la vuelta. - Cronometra la petición con
Date.nowantes y después denexty registra cuánto tardó cadapathname. - Navega por varias rutas distintas y confirma que tu middleware se ejecuta para todas sin haber tocado una sola página.