Middleware
middleware.ts: redirecciones, reescrituras, cabeceras y cookies antes de cada petición, con config.matcher.
El middleware ejecuta código antes de que se complete una solicitud. Puede redirigir, reescribir, establecer cabeceras y cookies, o devolver una respuesta por sí mismo.
Convención
Crea middleware.ts en la raíz del proyecto (o en src/), junto a app/. Exporta una función middleware (o una exportación predeterminada) y un config opcional.
import { NextResponse, type NextRequest } from "@textjs/app/server";export function middleware(request: NextRequest) { if (!request.cookies.has("session")) { return NextResponse.redirect(new URL("/login", request.url)); } return NextResponse.next();}export const config = { matcher: ["/dashboard/:path*", "/api/:path*"],};El middleware se ejecuta antes que los manejadores de ruta, los archivos estáticos y las páginas de consola, en text dev, text start y cada adaptador de despliegue de servidor. Usa las API web Request y Response, por lo que el mismo archivo se ejecuta en Node, Bun, Vercel Edge y Cloudflare Workers.
Coincidencia de rutas
Sin un config.matcher, el middleware se ejecuta para cada solicitud. Los patrones de coincidencia usan la misma sintaxis que Next.js:
| Patrón | Coincidencias |
|---|---|
/about | /about y /about/ |
/blog/:slug | /blog/hello |
/docs/:path* | /docs, /docs/a/b |
/a/:rest+ | /a/b, pero no /a |
/user/:id(\\d+) | /user/42, pero no /user/ada |
/((?!api/).*) | Todo excepto /api/* |
Añade condiciones con has y missing (cabecera, cookie, consulta o host):
export const config = { matcher: [{ source: "/beta/:path*", has: [{ type: "cookie", key: "beta", value: "1" }] }],};Los patrones se aplican a la ruta de acceso sin basePath. En text dev, las solicitudes de módulos del propio Vite (/@vite/*, archivos fuente) nunca ejecutan el middleware.
Cada host determina una ruta de acceso canónica antes de ejecutar el middleware y la usa para la coincidencia de patrones, request.nextUrl y la búsqueda de rutas, manejadores y archivos. Se decodifican las secuencias de escape con porcentaje y se vuelve a codificar la ruta tal como la escribe new URL(), se resuelven los segmentos . y .., y se eliminan las barras repetidas y finales. /ad%6din//users/ llega al middleware como /admin/users, mientras que /café se mantiene como /caf%C3%A9. Un patrón escrito con una barra final, como /admin/, coincide con /admin. Las solicitudes cuya ruta contiene una barra o barra inversa codificada (%2F, %5C), un carácter de control o una codificación con porcentaje no válida reciben un 400. Las rutas distinguen entre mayúsculas y minúsculas: /Admin no coincide con /admin, y los archivos estáticos solo se sirven con su nombre exacto.
Incluye en los patrones las URL en las que se sirve un archivo, no solo su nombre. Cloudflare Pages también sirve report.html en /report, por lo que un patrón para /report.html no protege /report. Enumera ambas, matcher: ["/report", "/report.html"], o guarda los archivos protegidos en un directorio y usa un patrón que lo cubra, como /reports/:path* para los archivos de public/reports/.
NextResponse
| API | Efecto |
|---|---|
NextResponse.next() | Continuar a la ruta |
NextResponse.redirect(url, status?) | 307 de forma predeterminada; url debe ser absoluta |
NextResponse.rewrite(url) | Servir otra ruta; los orígenes externos se sirven mediante un proxy |
NextResponse.json(body, init?) | Responder con JSON |
new Response(...) | Responder con cualquier contenido |
No devuelvas nada para continuar sin cambios. next() y rewrite() aceptan { status } para cambiar el estado de la respuesta final. Solo las respuestas creadas por NextResponse.next() / NextResponse.rewrite() controlan el enrutamiento: cualquier otra Response, incluida una devuelta por fetch(), se envía al cliente tal cual, con sus cabeceras x-middleware-* eliminadas.
request.nextUrl excluye basePath de pathname, mientras que sus valores href y toString() lo incluyen. Para permanecer dentro de la app, clónalo: const url = request.nextUrl.clone(); url.pathname = "/login"; return NextResponse.redirect(url);.
export function middleware(request: NextRequest) { if (request.nextUrl.pathname === "/status") { return NextResponse.rewrite(new URL("/api/health", request.url)); }}Cabeceras
Pasa request.headers para cambiar lo que reciben los manejadores posteriores, y headers para añadir cabeceras de respuesta:
export function middleware(request: NextRequest) { const headers = new Headers(request.headers); headers.set("x-user", "ada"); const response = NextResponse.next({ request: { headers } }); response.headers.set("x-frame", "▼"); return response;}Las cabeceras de respuesta del middleware se añaden a la respuesta final, salvo que el manejador de ruta establezca la misma cabecera.
Cookies
request.cookies lee la cabecera Cookie; response.cookies escribe Set-Cookie.
export function middleware(request: NextRequest) { const theme = request.cookies.get("theme")?.value ?? "geistDark"; const response = NextResponse.next(); response.cookies.set("theme", theme, { httpOnly: true, sameSite: "lax", maxAge: 60 * 60 * 24 }); return response;}Las cookies establecidas en una respuesta de next() o rewrite() también son visibles para el manejador de ruta en la misma solicitud.
Trabajo en segundo plano
El segundo argumento tiene waitUntil(), que mantiene activos los entornos de ejecución sin servidor después de enviar la respuesta:
import type { NextFetchEvent, NextRequest } from "@textjs/app/server";export function middleware(request: NextRequest, event: NextFetchEvent) { event.waitUntil(fetch("https://analytics.example.com", { method: "POST", body: request.url }));}Notas
- Las páginas renderizadas en el cliente se renderizan en el navegador, por lo que una reescritura a otra página cambia el archivo que se sirve, no la ruta que renderiza la app de consola. Las reescrituras a manejadores de ruta, archivos estáticos y orígenes externos se comportan como se espera.
- El middleware puede leer el cuerpo de la solicitud; los manejadores de ruta siguen recibiéndolo. El cuerpo de las solicitudes excluidas por un patrón nunca se modifica ni se lee.
request.urlsiempre tiene el origen del servidor: los destinos de solicitud como//other.host/xse normalizan a una sola barra inicial.- El adaptador estático no puede ejecutar middleware.