[]previous and next page

Middleware on routes and folders

A route module can export middleware: an array of middleware, outermost first, that runs before the route's beforeLoad and loaders.

For agentsUpdated

A route module can export middleware: an array of middleware, outermost first, that runs before the route's beforeLoad and loaders. A _middleware.ts file in a folder applies the same way to every page and API route beneath that folder. Both run on the server only: their code is removed from the browser build, so they may import server modules.

src/routes/admin/_middleware.tsts
import { type Middleware, redirect } from "@shadow-js/dusk/server";

/** Every page and API route under `admin/` runs this first. A visitor without a session goes to `/login`. */
export const middleware: Middleware[] = [
  async (event, next) => {
    if (event.cookies.get("session") === undefined) throw redirect("/login");
    return next();
  },
];
src/routes/admin/users.tsts
export const loader = () => ({ names: ["ada", "grace"] });

A middleware can also serve the request with another route of the app, the way Next's NextResponse.rewrite does: return rewrite("/internal/path", { headers }) from a middleware (or a route's middleware). The URL the browser sees stays the same, the target route answers, and the headers of the rewrite are added to the request the target receives. The app-wide middleware does not run again for the target, and a second rewrite of the same request is a 500, so a rewrite cannot loop. The destination must be a path of this app: an absolute URL, a //host and a path under /_dusk/ are refused with a TypeError.

The order of one request is fixed:

  • A page request: the handler-wide middleware of createHandler, then the _middleware.ts files of the folders above the page, outermost first, then the middleware export of each matched route (the root layout first, the page last), then beforeLoad and the loaders, then the render.
  • An API request: the handler-wide middleware, then the _middleware.ts files above the route, then the module's middleware export, then the method's own middleware and guard option, then the method.

A request that the host, size, CSRF or method checks refuse never reaches any of these. Middleware answers as it does for the handler: return a Response, throw redirect() or an HttpError, or let an error escape (the client gets a 500 with no detail). The rules of Middleware and error responses apply.

_middleware is a reserved name: the file is not a route, and no route can be called _middleware. A folder's file reaches pages and API routes only. Server functions have no folder in their URL (/_dusk/fn/<id>); guard them with their own guard option (see Guards).

Client navigations. The browser does not have this code. Before the router loads a top-level route whose subtree runs middleware, it posts the target path to {base}/_dusk/nav. The server runs the middleware of that path with no loader and no render, and answers. An allowed navigation continues. A redirect from the chain becomes the router's redirect. Any other refusal fails the navigation with the status, and the route's error boundary shows it. The loaders run after that, as the route says. A page whose chain runs middleware is never answered from the ISR cache, so its check runs on every request.

A check in a layout's middleware or beforeLoad decides what the page shows. It does not protect the data: each server function a loader calls runs its own guard, because anyone can post to /_dusk/fn/<id> without loading the page.