wandres.dev
AUTH Y MIDDLEWARE · sesiones y protección

Middleware: createMiddleware, onRequest y locals tipados

El middleware intercepta cada petición y respuesta del servidor para tareas transversales: cabeceras, redirecciones, telemetría y, sobre todo, poblar `event.locals` con datos de la petición. Se declara con `createMiddleware` de `@solidjs/start/middleware`, se registra en `app.config.ts`, y corre en dos momentos: `onRequest` antes del handler y `onBeforeResponse` después. Este capítulo tipa `event.locals` por fusión de declaraciones y explica el límite crítico que decide toda tu arquitectura de auth: el middleware no corre en las navegaciones de cliente, así que no es una frontera de seguridad.

⏱ 17 min

Entre la llegada de una petición HTTP y la respuesta que sale hay un espacio donde conviene ejecutar lógica transversal —la que no pertenece a una ruta concreta sino a todas—: medir tiempos, fijar cabeceras de seguridad, normalizar rutas, o depositar en un maletín común datos que el resto del servidor necesitará. Ese espacio es el middleware. En SolidStart lo declaras con createMiddleware, lo registras en app.config.ts y lo enganchas a dos instantes del ciclo: onRequest, antes de que el handler toque la petición, y onBeforeResponse, justo antes de despachar la respuesta. Su pieza más valiosa es event.locals, un objeto por petición que comunica el middleware con cualquier función de servidor. Pero su límite —que no corre en toda navegación— es lo que de verdad tienes que grabarte.

🎯 Al terminar esta lección sabrás
  • Declarar un middleware con createMiddleware y registrarlo con middleware en app.config.ts.
  • Distinguir onRequest (antes del handler) de onBeforeResponse (después, antes de despachar).
  • Tipar event.locals por fusión de declaraciones sobre RequestEventLocals y leerlo con getRequestEvent.
  • Interiorizar que el middleware no corre en navegaciones de cliente y por qué eso lo descarta como guardia de autorización.

createMiddleware: interceptar la petición

El middleware es un objeto de configuración que exportas por defecto desde un archivo dedicado. createMiddleware le da forma y tipos; app.config.ts le dice a SolidStart dónde encontrarlo. Sin ese registro, el archivo es inerte.

// src/middleware/index.ts
import { createMiddleware } from "@solidjs/start/middleware";

export default createMiddleware({
  onRequest: (event) => {
    event.locals.inicio = Date.now();            // guarda para medir despues
  },
  onBeforeResponse: (event) => {
    const ms = Date.now() - event.locals.inicio; // lee lo que dejo onRequest
    event.response.headers.set("Server-Timing", `total;dur=${ms}`);
  },
});
// app.config.ts
import { defineConfig } from "@solidjs/start/config";

export default defineConfig({
  middleware: "src/middleware/index.ts",
});

El event que reciben los handlers es un FetchEvent: expone event.request (la Request estándar), event.response (con sus headers mutables), event.locals (el maletín por petición) y event.nativeEvent (el evento h3 subyacente, que necesitan los helpers de cookies de vinxi).

onRequest y onBeforeResponse: dos momentos del ciclo

Los dos ganchos ocupan lados opuestos del handler. onRequest corre antes: es el sitio para poblar event.locals, ajustar cabeceras de entrada o cortar la petición con una redirección temprana. onBeforeResponse corre después de que el handler produjo su resultado pero antes de enviarlo: el sitio para fijar cabeceras de salida o registrar métricas de la respuesta.

Devolver un valor desde onRequest cortocircuita todo el pipeline: ni corren más middlewares ni se ejecuta el handler; ese valor se convierte en la respuesta. Solo se admiten objetos Response, y Solid Router te da atajos para los casos comunes con redirect y json.

import { createMiddleware } from "@solidjs/start/middleware";
import { redirect } from "@solidjs/router";

export default createMiddleware({
  onRequest: (event) => {
    const { pathname } = new URL(event.request.url);
    if (pathname === "/viejo") return redirect("/nuevo", 301); // corta aqui
  },
});
flowchart TD
A[llega la peticion] --> B[onRequest corre]
B -->|devuelve un Response| C[cortocircuito: respondo ya]
B --> D[handler de ruta o funcion de servidor]
D --> E[onBeforeResponse corre]
E --> F[respuesta al cliente]
style B fill:#89b4fa,color:#11111b
style C fill:#f38ba8,color:#11111b
style E fill:#f9e2af,color:#11111b

event.locals: el maletín tipado de la petición

event.locals es un objeto plano, vivo solo durante una petición y borrado al terminar, pensado para compartir datos entre el middleware y cualquier código de servidor: el estado de autenticación, un identificador de traza, metadatos del cliente. Dentro del middleware lo lees y escribes directo; en cualquier otro contexto de servidor lo alcanzas con getRequestEvent de solid-js/web.

Como es un objeto libre, TypeScript no conoce sus campos hasta que se los declaras. La forma canónica es la fusión de declaraciones sobre la interfaz RequestEventLocals del módulo @solidjs/start/server: la amplías una vez y todo event.locals de tu app queda tipado.

// src/global.d.ts
import "@solidjs/start/server";

declare module "@solidjs/start/server" {
  interface RequestEventLocals {
    inicio: number;
    user: { id: string; email: string } | null;
  }
}
// Leer locals desde una funcion de servidor cualquiera
import { getRequestEvent } from "solid-js/web";
import { query } from "@solidjs/router";

export const getUser = query(async () => {
  "use server";
  const event = getRequestEvent();
  return event?.locals.user ?? null;   // lo que puso el middleware
}, "user");

Cuando la lógica transversal crece, no la amontones en una función gigante: onRequest y onBeforeResponse aceptan también arrays de funciones que corren en orden, cada una tipada como FetchEvent. Así compones middlewares pequeños y enfocados, colocando cada uno después de aquellos de los que depende.

import { createMiddleware } from "@solidjs/start/middleware";
import type { FetchEvent } from "@solidjs/start/server";

function sello(event: FetchEvent) {
  event.locals.inicio = Date.now();
}
function contexto(event: FetchEvent) {
  // depende de nada previo; poblara event.locals.user en el nivel de auth
}

export default createMiddleware({
  onRequest: [sello, contexto], // corren en este orden
});

El límite: no es una frontera de seguridad

Aquí está la advertencia que lo cambia todo. El middleware no corre en cada petición: en particular, no corre durante las navegaciones de cliente, donde el router puede reutilizar datos ya cargados sin volver a tocar el servidor. Confiar en él para autorizar —«el middleware comprobó la sesión, luego el usuario está autenticado»— abre un agujero de seguridad, porque hay caminos que llegan a tus datos sin haber pasado por él.

⚠️
Middleware para poblar contexto, nunca para decidir autorización

La regla operativa de SolidStart es tajante: las comprobaciones de autorización van lo más cerca posible de la fuente de datos —dentro de la función de servidor, la query o la ruta de API que sirve el recurso—, no en el middleware. Úsalo para poblar event.locals con el usuario resuelto de la sesión, para cabeceras, para redirecciones de conveniencia; pero la frase «¿tiene permiso este usuario?» se responde junto al dato, donde ninguna ruta puede saltársela. Un guardia que a veces no corre no es un guardia: es una falsa sensación de seguridad, y de las caras.

Dos instantes

onRequest antes del handler para poblar y redirigir; onBeforeResponse despues para cabeceras y metricas de salida.

🧳

locals por peticion

Un objeto que nace y muere con la peticion. Tipalo por fusion de declaraciones y leelo con getRequestEvent.

🚧

No es un guardia

No corre en navegaciones de cliente. Sirve para contexto transversal, no para la decision de autorizar.

El middleware es una costura transversal, no un portero: separa poblar de decidir

La madurez con el middleware llega cuando dejas de verlo como «el sitio donde protejo la app» y empiezas a verlo como una costura transversal cuyo trabajo es preparar el terreno, no juzgarlo. Hay dos verbos que la ingeniería novata confunde y que aquí conviene divorciar para siempre: poblar y decidir. Poblar es resolver, una vez por petición, datos que muchos consumidores necesitarán —quién es el usuario detrás de esta cookie, qué región, qué traza— y dejarlos en event.locals para que nadie tenga que recalcularlos. Decidir es responder a la pregunta de autorización —¿puede este usuario ver este recurso?—, y esa respuesta pertenece al borde del dato, dentro de la misma función de servidor que lo devuelve, porque solo ahí tienes la garantía de que se ejecuta siempre que alguien pide el recurso. El middleware es un lugar excelente para lo primero y un lugar traicionero para lo segundo, y la razón es puramente operativa: su ejecución no está garantizada antes de cada acceso a datos. El router de Solid puede servir una navegación de cliente con una query cacheada sin rozar el servidor, de modo que un usuario podría llegar a una vista «protegida» sin que tu middleware haya corrido una sola vez en esa transición. Si la decisión vivía allí, no se tomó. Si vivía junto al dato —en la query que hace throw redirect cuando no hay sesión, como verás en las próximas lecciones—, se toma inevitablemente, porque no hay forma de obtener el dato sin invocar la función que primero pregunta si puedes. Interioriza este reparto y tu arquitectura de auth se vuelve robusta por construcción: el middleware pobla event.locals.user como una comodidad de la petición, y cada recurso sensible vuelve a comprobar la autoridad en su propia puerta. Uno prepara, el otro decide; y nunca al revés.

⚔️ Puebla, corta y mide con middleware
  1. Crea src/middleware/index.ts con un onRequest que guarde event.locals.inicio y un onBeforeResponse que fije una cabecera Server-Timing; regístralo en app.config.ts y confírmalo en la pestaña de red.
  2. Amplía RequestEventLocals en un .d.ts con inicio y user; comprueba que TypeScript ahora autocompleta event.locals en todo el proyecto.
  3. Añade una redirección temprana en onRequest para una ruta legada devolviendo redirect(destino, 301) y verifica que el handler original nunca llega a correr.
  4. Lee event.locals desde una query con getRequestEvent y observa que ves lo que el middleware depositó en la misma petición.
  5. Argumenta, con el caso de una navegación de cliente que reutiliza una query cacheada, por qué poner la decisión de autorización en el middleware dejaría una vista protegida accesible sin comprobación.