Fresh logo

Middleware

A middleware is defined in a _middleware.tsx file inside the routes/ directory. It intercepts the request before the route’s handler runs. This lets you perform custom logic before or after the handler, modify or inspect the request and response. Common use cases are logging, authentication, setting response headers, and computing state that the rest of the chain can read.

Each middleware receives a ctx argument. Call ctx.next() to invoke the next middleware (or the route handler, if this is the last one). Whatever that returns is the eventual Response. The same ctx has a state property used to pass arbitrary data downstream. Handlers, pages, and layouts below this middleware can read ctx.state (or, in components, props.state). State flows along the chain by passing a new object to next, like ctx.next({ user }). The shape is typed by the State interface you export. The generated ParentState type carries forward whatever state ancestors already added. See Type checking.

Typescript routes/_middleware.tsx
import { middleware } from "./$_middleware.ts";

export interface State {
  startedAt: number;
}

export default middleware(async (ctx) => {
  const res = await ctx.next({ startedAt: Date.now() });
  res.headers.set("server", "fresh");
  return res;
});
Typescript routes/index.tsx
import { page } from "./$index.ts";

export default page((props) => {
  // props.state.startedAt is typed from the middleware's State
  return <p>Request started at {props.state.startedAt}.</p>;
});

Middlewares are scoped and can be layered. A project can have any number of middleware files, each covering a different set of routes. If multiple middlewares cover the same route, all of them run, in order of specificity (least specific first).

For example, take a project with the following routes:

Text Project structure
└── <root>/routes
    ├── _middleware.tsx
    ├── index.tsx
    └── admin
        ├── _middleware.tsx
        ├── index.tsx
        └── signin.tsx

For a request to /, the flow is:

  1. routes/_middleware.tsx runs.
  2. Calling ctx.next() invokes the routes/index.tsx handler.

For a request to /admin, the flow is:

  1. routes/_middleware.tsx runs.
  2. Calling ctx.next() invokes routes/admin/_middleware.tsx.
  3. Calling ctx.next() invokes the routes/admin/index.tsx handler.

For a request to /admin/signin, the flow is:

  1. routes/_middleware.tsx runs.
  2. Calling ctx.next() invokes routes/admin/_middleware.tsx.
  3. Calling ctx.next() invokes the routes/admin/signin.tsx handler.

Middleware also has access to route parameters. If you have a routes/[tenant]/admin/_middleware.tsx like the following, and the request is for mysaas.com/acme/admin/, then ctx.params.tenant is "acme" inside the middleware.

Typescript routes/[tenant]/admin/_middleware.tsx
import { middleware } from "./$_middleware.ts";

export default middleware(async (ctx) => {
  const currentTenant = ctx.params.tenant;
  // do something with the tenant
  return ctx.next();
});

Short-circuiting

Returning a Response instead of calling ctx.next() stops the chain. The rest of the middlewares and the route handler are skipped, and the response goes straight back to the client. This is how you implement auth gates.

Typescript routes/admin/_middleware.tsx
import { middleware } from "./$_middleware.ts";

export default middleware((ctx) => {
  if (!isLoggedIn(ctx.req)) {
    return ctx.redirect("/login", 303);
  }
  return ctx.next();
});

To redirect from middleware, prefer ctx.redirect. It guards against protocol-relative URLs. If you’d rather build the response yourself:

Typescript routes/_middleware.tsx
import { middleware } from "./$_middleware.ts";

export default middleware(() => {
  return new Response("", {
    status: 307,
    headers: { Location: "/my/new/relative/path" },
  });
});

307 is a temporary redirect. Use 301 (or 308) for a permanent one.

Extending state

ctx.next(state) passes the entire new state. There is no automatic merging. If your middleware should add fields to whatever the parent middlewares already added, spread the existing state or extend ParentState.

Typescript routes/admin/_middleware.tsx
import { middleware, ParentState } from "./$_middleware.ts";

export interface State extends ParentState {
  user: { id: string; isAdmin: boolean };
}

export default middleware(async (ctx) => {
  const user = await loadUser(ctx);
  if (!user.isAdmin) return new Response("Forbidden", { status: 403 });
  return ctx.next({ ...ctx.state, user });
});

Routes nested under admin/ then see both the parent state and ctx.state.user, fully typed.

Global middleware

To run middleware for every route, ahead of all file-based middleware, export an App from a root entry.server.ts and register callbacks with app.use().

Typescript entry.server.ts
import { App } from "fresh";

export const app = new App().use((ctx) => {
  // Redirect an old host to the new one.
  if (ctx.url.hostname === "old.example.com") {
    const url = new URL(ctx.url);
    url.hostname = "example.com";
    return Response.redirect(url, 308);
  }
  return ctx.next();
});

Each app.use(fn) runs like a rootmost _middleware, before any route’s file-based middleware. app.use() is chainable, so you can register several callbacks in a row.