El Middleware es un código que se ejecuta antes de que Astro procese cualquier página o endpoint. Nos permite interceptar la petición, modificarla o bloquearla completamente.
Si has trabajado con frameworks de backend como Express o Koa, el concepto te resultará familiar.
A diferencia de otros frameworks donde el middleware solo existe si tienes un servidor Node.js corriendo, en Astro el middleware está disponible en ambos modos, aunque su momento de ejecución cambia:
En modo SSR (output: ‘server’): El middleware se ejecuta en tiempo de solicitud. Cada vez que un usuario visita una página, el middleware se activa. Es ideal para autenticación, lectura de cookies o redirecciones dinámicas.
En modo SSG (output: ‘static’): El middleware se ejecuta en tiempo de compilación (Build time). Se activa cuando haces npm run build mientras Astro genera las páginas HTML. Es útil para manipular el HTML final, inyectar datos en la compilación o generar logs de construcción.
En este artículo nos centraremos principalmente en los casos de uso dinámicos (SSR), que es donde el middleware tiene más sentido (protección de rutas, usuarios), pero ten en cuenta que la arquitectura es la misma para ambos
Creando el Middleware
En Astro, el middleware se define en un único archivo especial: src/middleware.ts (o .js). No puedes ponerle otro nombre ni moverlo de carpeta.
La estructura básica es la siguiente:
// src/middleware.ts
import { defineMiddleware } from "astro:middleware";
// La función 'onRequest' intercepta TODAS las peticiones
export const onRequest = defineMiddleware(async (context, next) => {
console.log("Interceptada petición a:", context.url.pathname);
// 'next()' pasa la pelota al siguiente paso (renderizar la página)
// Debemos devolver su respuesta
const response = await next();
console.log("Página renderizada. Devolviendo respuesta...");
return response;
});
Este código actúa como un “sandwich”. Hacemos cosas antes de next() (pre-procesamiento) y cosas después de next() (post-procesamiento).
Caso de uso 1: Protección de rutas
El uso más común es proteger una sección de la web. Vamos a implementar una lógica simple: si intentas entrar en /dashboard y no tienes la cookie de sesión, te mandamos al login.
// src/middleware.ts
import { defineMiddleware } from "astro:middleware";
export const onRequest = defineMiddleware(async (context, next) => {
// 1. Definimos qué rutas son privadas
const isPrivate = context.url.pathname.startsWith('/dashboard');
// 2. Comprobamos si existe la cookie de sesión (solo en modo SSR)
const hasSession = context.cookies.has('user_session');
// 3. Lógica de bloqueo
if (isPrivate && !hasSession) {
// Si es privado y no tiene sesión, redirigimos
return context.redirect('/login');
}
// 4. Si todo está bien, dejamos pasar
return next();
});
Ahora, cualquier archivo que crees dentro de src/pages/dashboard/ estará protegido automáticamente. No necesitas tocar ni una línea de código en esas páginas.
Caso de uso 2: Inyección de datos)
El middleware no solo sirve para bloquear. También sirve para preparar datos.
Imagina que ya has verificado la cookie y sabes quién es el usuario. Sería ineficiente que la página vuelva a leer la cookie y buscar al usuario en la base de datos. El middleware ya lo ha hecho.
Para pasar datos del Middleware a las Páginas, usamos el objeto context.locals.
// src/middleware.ts
import { defineMiddleware } from "astro:middleware";
import { getUserFromDB } from "./lib/db";
export const onRequest = defineMiddleware(async (context, next) => {
const token = context.cookies.get('token')?.value;
if (token) {
// Buscamos el usuario y lo guardamos en 'locals'
const user = await getUserFromDB(token);
if (user) {
context.locals.user = user;
}
}
return next();
});
Ahora, en cualquier componente .astro, podemos acceder a ese usuario directamente:
---
// src/pages/perfil.astro
const { user } = Astro.locals;
// Si el middleware no encontró usuario, redirigimos
if (!user) return Astro.redirect('/login');
---
<h1>Bienvenido, {user.name}</h1>
Encadenando Middlewares (sequence)
En aplicaciones grandes, tener todo el código en una sola función onRequest puede ser un caos. Podrías tener lógica de autenticación, lógica de traducción (i18n), lógica de analítica, etc.
Astro nos permite dividir el middleware en funciones pequeñas y encadenarlas usando sequence.
// src/middleware.ts
import { sequence } from "astro:middleware";
async function auth(context, next) {
console.log("1. Comprobando Auth");
// Lógica de auth...
return next();
}
async function validation(context, next) {
console.log("2. Validando datos");
// Lógica de validación...
return next();
}
async function logging(context, next) {
console.log("3. Registrando visita");
return next();
}
// Exportamos la secuencia en orden
export const onRequest = sequence(auth, validation, logging);
El orden importa: auth se ejecutará primero. Si auth decide redirigir y no llamar a next(), validation y logging nunca se ejecutarán.
Manipulación de la respuesta
A veces queremos modificar la respuesta HTML después de que la página se haya renderizado. Por ejemplo, para añadir cabeceras de seguridad o minificar el HTML.
export const onRequest = defineMiddleware(async (context, next) => {
const response = await next(); // Esperamos a que se genere el HTML
// Modificamos headers
response.headers.set("X-Powered-By", "Astro + LuisLlamas");
// Incluso podemos modificar el HTML (cuidado con el rendimiento)
const html = await response.text();
const newHtml = html.replace("</body>", "</body>");
return new Response(newHtml, {
status: 200,
headers: response.headers
});
});
Interceptar y reescribir el cuerpo HTML (response.text()) tiene un coste de rendimiento alto. Úsalo solo si es estrictamente necesario.
