wandres.dev
MIDDLEWARE · onRequest y locals

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.

⏱ 18 min

Ya tienes las piezas: onRequest como punto único, context.locals como canal tipado, la respuesta transformable a la vuelta, el corte de la cadena a la ida, y sequence para componer varios eslabones en orden. Este capítulo cierra el nivel poniéndolas a trabajar en los cinco encargos que aparecen una y otra vez en producción —autenticación, idioma, registro, seguridad y experimentos— para que veas que no son cinco técnicas distintas, sino la misma forma aplicada cinco veces. Cada caso es un middleware pequeño y enfocado; el arte final está en ordenarlos con criterio dentro de una sola secuencia.

🎯 Al terminar esta lección sabrás
  • Proteger zonas privadas resolviendo la sesión en context.locals.
  • Resolver el idioma leyéndolo del primer segmento de la URL.
  • Registrar cada petición en una línea estructurada con estado y latencia.
  • Blindar todas las respuestas con cabeceras y repartir tráfico A/B estable.

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.ts
import { 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.ts
import { 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.ts
export 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.ts
export 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.ts
export 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.ts
import { 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);
flowchart TD
RQ[peticion] --> LOG[logging cronometra]
LOG --> SEG[seguridad prepara]
SEG --> AUTH{sesion valida}
AUTH -->|no y ruta privada| RED[redirect a entrar]
AUTH -->|si| I18N[i18n resuelve idioma]
I18N --> AB[experimento asigna grupo]
AB --> REN[render de la ruta]
REN --> OUT[respuesta con cabeceras y log]
style RQ fill:#89b4fa,color:#11111b
style RED fill:#f38ba8,color:#11111b
style OUT fill:#a6e3a1,color:#11111b
⚠️
El middleware corre en cada ruta: cuida su coste

Como el middleware se ejecuta para toda petición, cualquier trabajo pesado que metas ahí se paga en cada visita. Una consulta a base de datos por petición para resolver la sesión es normal; hacer varias, o llamar a un servicio externo lento sin caché, multiplica la latencia de todo el sitio a la vez. La misma centralidad que hace poderoso al middleware lo vuelve un cuello de botella si lo descuidas. Mantén cada eslabón barato, cachea lo que puedas, y reserva las operaciones costosas para las rutas que de verdad las necesiten, no para el portero por el que pasan todas.

🔐

Auth

Resuelve la sesion en locals para todos y veta las zonas privadas cortando la cadena antes del render.

🌍

i18n

Lee el idioma del primer segmento de la URL una sola vez y lo publica en locals para todo el arbol.

📊

Logging y seguridad

A la vuelta: una linea estructurada por peticion y cabeceras que blindan cada respuesta del sitio.

🧪

A/B testing

Asigna un grupo estable por cookie y lo deja en locals para servir la variante sin recalcularla.

El middleware es la costura donde vive todo lo que es cierto para toda petición

Al cerrar este nivel conviene ver los cinco casos no como una lista de recetas sino como una sola revelación repetida: existe, en toda aplicación web, una categoría de lógica que no pertenece a ninguna página en particular porque pertenece a todas, y esa lógica necesita un hogar. Quién eres, qué idioma hablas, qué se registró de tu visita, cómo se blindó tu respuesta, en qué mitad de un experimento caíste: ninguna de esas preguntas la responde una página, todas las responde el sitio, y responderlas página por página sería copiar la misma verdad en cien sitios hasta que uno diverja y mienta. El middleware es el hogar que faltaba: la costura entre el mundo exterior y tu aplicación, el único punto por el que —sin excepción— entra cada petición y sale cada respuesta. Poner ahí lo que es cierto para toda petición no es un truco de rendimiento, es poner cada cosa en su sitio ontológico. Y una vez que lo ves así, los cinco casos se funden en una forma única: cada uno resuelve algo en el borde y lo publica hacia dentro por locals, o inspecciona la respuesta y la ajusta en la vuelta, o decide si la petición merece seguir viva. Resolver, publicar, inspeccionar, ajustar, vetar: ese es todo el vocabulario, y con él se escribe la autenticación igual que la internacionalización, el registro igual que la seguridad, el experimento igual que el enrutado condicional. La disciplina que separa un middleware sano de un monstruo es la que ya conoces —una responsabilidad por eslabón, un orden que respeta las dependencias, un coste vigilado porque se paga en cada visita—, pero la idea que de verdad quiero que te lleves es más honda: que un sitio web bien pensado no es una pila de páginas independientes, sino un cuerpo con un sistema nervioso central, y que ese centro —el lugar donde convergen las verdades transversales y desde donde se gobiernan— es precisamente esto que acabas de aprender a habitar. Cuando lo interiorizas, dejas de preguntarte en qué página pongo esto y empiezas a preguntarte esto, es cierto para una página o para todas; y el día que la respuesta es para todas, ya sabes exactamente dónde vive.

⚔️ Monta el borde completo de un sitio real
  1. Escribe auth: resuelve la sesión en context.locals.usuario para toda petición y redirige a /entrar cuando falte sesión en una ruta bajo /panel.
  2. Escribe i18n: lee el primer segmento de la URL, valídalo contra tus idiomas soportados y publícalo en context.locals.idioma con un valor por defecto.
  3. Escribe logging y seguridad: una línea estructurada por petición con método, ruta, estado y milisegundos, y las cabeceras de seguridad sobre cada respuesta.
  4. Escribe experimento con un grupo estable por cookie y compón los cinco con sequence, razonando por qué el registro va primero y el experimento al final.