defineRouteMiddleware
Declares global or named route middleware in app/middleware, executed before the page in SSR and on each browser navigation.
Se autoimporta en las aplicaciones del framework.
import { defineRouteMiddleware } from "@meekojs/meeko/framework";defineRouteMiddleware declares the route middleware exported by default from a file in app/middleware/. It executes before route options and the page loader: on the server during SSR and in the browser on each navigation, with the same code. Returning undefined continues; returning a Response (usually a redirect) stops the route.
There are two kinds:
- Global (without options): runs on every route, in filename order.
- Named (since
1.6,{ global: false }): only on routes that request it throughmiddlewareindefinePageor a+group.ts. The name is the filename without.tsor a numeric prefix:10-auth.tsis"auth".
It is auto-imported. It is isomorphic: it also reaches the browser bundle.
Usage#
// app/middleware/auth.ts
import { defineRouteMiddleware } from "@meekojs/meeko/framework";
export default defineRouteMiddleware(({ url, phase, request }) => {
// A public cookie indicating the session: Meeko's session cookie is HttpOnly.
const cookies = phase === "server" ? request?.headers.get("cookie") ?? "" : document.cookie;
if (!cookies.includes("signed-in=1")) {
const login = new URL("/login", url);
login.searchParams.set("next", url.pathname);
return Response.redirect(login);
}
}, { global: false });Pages requesting it with definePage({ middleware: ["auth"] }) or a +group.ts redirect to /login without the cookie.
Type#
function defineRouteMiddleware(
middleware: RouteMiddleware,
options?: RouteMiddlewareOptions,
): RouteMiddlewareinterface RouteMiddlewareContext {
readonly url: URL
readonly match: RouteMatch
readonly signal: AbortSignal
readonly phase: 'server' | 'client'
readonly request?: Request
}interface RouteMiddlewareOptions {
readonly global?: boolean
}Parameters#
| Parameter | Type | Default | Description |
|---|---|---|---|
middleware |
(context: RouteMiddlewareContext) => void | Response | Promise<void | Response> |
required | The function. Receives url, match (the route and its params), signal (aborted if navigation is canceled), phase ("server" or "client") and request, which only exists on the server. |
options |
{ global?: boolean } |
global | Since 1.6. { global: false } makes it named. Must be a literal object: Meeko reads it without executing the file. |
Return value#
The same middleware, to export as default. What each result does:
| Result | On the server | In the browser |
|---|---|---|
undefined |
Continues to the next middleware and the page | Same |
Response.redirect(url) |
Responds with the redirect | location.assign with the URL: a new page load |
Another Response |
Returns it as the response | Cancels navigation and passes it to onNavigationError |
| An error | Error page | Cancels navigation |
Examples#
Global, limited to one route#
Global middleware can decide based on the route with match.route.id:
// app/middleware/10-locale.ts
import { defineRouteMiddleware } from "@meekojs/meeko/framework";
export default defineRouteMiddleware(({ url, match }) => {
if (match.route.id !== "/dashboard") return;
if (!url.searchParams.has("locale")) {
const target = new URL(url);
target.searchParams.set("locale", "es");
return Response.redirect(target);
}
});Request it from a group and a page#
// app/pages/(dashboard)/+group.ts
import { defineGroup } from "@meekojs/meeko/framework";
export default defineGroup({ layout: "dashboard", middleware: ["auth"] });<!-- app/pages/(dashboard)/admin/users.meeko -->
<script server lang="ts">
export const route = definePage({ middleware: ["admin"] });
</script>
<template><h1>Users</h1></template>/admin/users runs global middleware, then auth (from the group), then admin (from the page). middleware: false removes inherited middleware.
Scheduled maintenance#
Global middleware can stop all routes except one:
// app/middleware/00-maintenance.ts
import { defineRouteMiddleware } from "@meekojs/meeko/framework";
const until = Date.parse("2026-11-01T06:00:00Z");
export default defineRouteMiddleware(({ url, match }) => {
if (Date.now() < until && match.route.id !== "/maintenance") return Response.redirect(new URL("/maintenance", url));
});Common mistakes#
Requesting middleware that does not exist#
export const route = definePage({ middleware: ["auth", "admn"] });meeko check, meeko dev and meeko build stop with el middleware "admn" no existe; crea app/middleware/admn.ts con defineRouteMiddleware(..., { global: false }) (middleware con nombre disponibles: "admin", "auth"), o app/middleware/admn.server.ts con defineServerRouteMiddleware para uno que sólo se ejecuta en el servidor.
export const route = definePage({ middleware: ["auth", "admin"] });Requesting global middleware by name#
// app/middleware/10-locale.ts, without options
export default defineRouteMiddleware(() => {});
// app/pages/(dashboard)/+group.ts
export default defineGroup({ middleware: ["locale"] });el middleware "locale" (…/10-locale.ts) es global y ya se ejecuta en todas las rutas; quítalo de middleware o decláralo con defineRouteMiddleware(..., { global: false }).
export default defineRouteMiddleware(() => {}, { global: false });Using it in a *.server.ts#
// app/middleware/auth.server.ts
export default defineRouteMiddleware(() => {}, { global: false });un archivo *.server.ts de app/middleware es middleware de ruta de servidor; decláralo con defineServerRouteMiddleware en lugar de defineRouteMiddleware.
// app/middleware/auth.server.ts
import { redirect } from "@meekojs/meeko/framework";
export default defineServerRouteMiddleware(({ session }) => {
if (!session.current) redirect("/login");
}, { global: false });Trusting it as authorization#
Isomorphic route middleware travels to the browser and does not read the session (its cookie is HttpOnly). A user can bypass it by requesting data from the API: no error points this out.
// app/middleware/admin.ts: the only role check
export default defineRouteMiddleware(({ url }) => {
if (!document.cookie.includes("role=admin")) return Response.redirect(new URL("/", url));
}, { global: false });// app/middleware/admin.server.ts: on the server, with the session
export default defineServerRouteMiddleware(({ session, url }) => {
if ((session.current?.data as { role?: string } | undefined)?.role !== "admin") return Response.redirect(new URL("/", url));
}, { global: false });And in the API, auth: "required" and a role check in the handler.
Notes#
- Server and browser. The same file executes in SSR (
phase: "server", withrequest) and client navigation (phase: "client", withoutrequest). Do not importserver/or secrets: they travel in the bundle. Hydration does not execute it: the server already did. - Order. Global middleware first, in filename order; then the route's named middleware, in declared order (groups from the outside in, then the page); then server middleware (
*.server.ts). The first to return aResponsestops the route. - Bundle. Global middleware ships with the browser entry point; each named middleware is a chunk loaded the first time you navigate to one of its routes.
- Security. Improves navigation (redirects without requesting anything), but never replaces authorization in the loader, API or RPC. To protect a page with the session, use
defineServerRouteMiddleware. - Exclusions.
*.test.ts,*.spec.ts,*.d.tsand*.client.tsfiles inapp/middlewareare not middleware. Only the project'sapp/middlewarecounts, not that of layers.