Casos reales: auth, i18n, logging, seguridad y A/B
El middleware puesto a trabajar en los cinco encargos que aparecen en casi todo sitio de producción: proteger zonas privadas con sesiones en locals, resolver el idioma leyéndolo de la URL, registrar cada petición en una línea estructurada, blindar todas las respuestas con cabeceras de seguridad y repartir tráfico de forma estable en un experimento A/B. Cada caso, una pieza enfocada; todas juntas, una secuencia bien ordenada.
Autenticación y sesiones: el guardián de las zonas privadas
El caso rey del middleware es la autenticación, porque reúne todo lo aprendido: resuelve la sesión una vez, la deja en locals para las páginas, y veta las zonas privadas cortando la cadena antes de renderizar. Un solo eslabón cubre el sitio entero: donde antes cada página protegida repetía su comprobación, ahora hay un único guardián en el borde.
// src/middleware/auth.tsimport { defineMiddleware } from 'astro:middleware';export const auth = defineMiddleware(async (context, next) => { const sid = context.cookies.get('sid')?.value; context.locals.usuario = await buscarSesion(sid); const privado = context.url.pathname.startsWith('/panel'); if (privado && !context.locals.usuario) { return context.redirect('/entrar'); } return next();});Fíjate en la doble función: siempre resuelve el usuario y lo publica en locals —incluso en las rutas públicas, donde la página lo usará para saludar o mostrar un menú—, pero solo corta cuando la ruta es privada y no hay sesión. Separar resolver de proteger es lo que hace este patrón limpio: el dato está disponible para todos, y el veto se aplica solo donde toca.
i18n: el idioma como una decisión de borde
Cuando el idioma vive en la URL —/es/blog, /en/blog—, el middleware es el lugar natural para leerlo. El primer segmento de la ruta es el idioma, así que un eslabón lo extrae, lo valida contra los soportados y lo publica en locals para que cada página y componente pinten en la lengua correcta sin volver a mirar la URL.
// src/middleware/i18n.tsimport { defineMiddleware } from 'astro:middleware';const IDIOMAS = ['es', 'en', 'fr'];export const i18n = defineMiddleware((context, next) => { const segmento = context.url.pathname.split('/')[1]; context.locals.idioma = IDIOMAS.includes(segmento) ? segmento : 'es'; return next();});Resuelto en el borde, el idioma deja de ser un problema de cada página y pasa a ser un dato de contexto, igual que el usuario. Si además quisieras que una visita a /blog sin prefijo aterrizara en el idioma por defecto, este es el sitio: un context.redirect hacia /es/blog a la ida, o un context.rewrite si prefieres servir la variante sin cambiar la barra. La política de idiomas, entera, en un solo archivo.
Observabilidad, seguridad y experimentos
Los tres encargos restantes comparten forma y demuestran las dos caras del ciclo. El registro vive a la vuelta, porque necesita el estado final: mide el tiempo alrededor de next y emite una línea estructurada por petición, el cimiento de toda observabilidad.
// src/middleware/logging.tsexport const logging = defineMiddleware(async (context, next) => { const inicio = Date.now(); const response = await next(); console.log(JSON.stringify({ metodo: context.request.method, ruta: context.url.pathname, estado: response.status, ms: Date.now() - inicio, })); return response;});La seguridad también vive a la vuelta, retocando las cabeceras de cada respuesta para que ninguna página quede sin protección. Fijadas una vez en el borde, blindan el sitio completo:
// src/middleware/seguridad.tsexport const seguridad = defineMiddleware(async (context, next) => { const response = await next(); response.headers.set('X-Frame-Options', 'DENY'); response.headers.set('X-Content-Type-Options', 'nosniff'); response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin'); return response;});El experimento A/B vive a la ida: asigna a cada visitante un grupo estable, guardado en una cookie para que no cambie entre visitas, y lo publica en locals para que la página sirva la variante correspondiente. El reparto se decide una vez y se recuerda.
// src/middleware/experimento.tsexport const experimento = defineMiddleware((context, next) => { let grupo = context.cookies.get('ab')?.value; if (!grupo) { grupo = Math.random() < 0.5 ? 'a' : 'b'; context.cookies.set('ab', grupo, { path: '/' }); } context.locals.grupo = grupo; return next();});Y ahora el remate, donde todo lo del capítulo anterior cobra sentido: los cinco se componen en una secuencia, y su orden codifica sus dependencias. El registro va primero para envolver y cronometrar todo; la seguridad, temprano, para que sus cabeceras cubran hasta las respuestas que cortan; la autenticación antes que el resto de la lógica, porque puede vetar; el idioma y el experimento al final, ya con la sesión resuelta.
// src/middleware.tsimport { sequence } from 'astro:middleware';import { logging } from './middleware/logging';import { seguridad } from './middleware/seguridad';import { auth } from './middleware/auth';import { i18n } from './middleware/i18n';import { experimento } from './middleware/experimento';export const onRequest = sequence(logging, seguridad, auth, i18n, experimento);