Saltar al contenido

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.

TypeScript
// 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.

TypeScript
// 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.

TypeScript
// 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:

TypeScript
// 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.

TypeScript
// 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.

TypeScript
// 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);