Middlewares
A middleware is defined in a _middleware.ts
file. It will intercept the
request in order for you to perform custom logic before or after the route
handler. This allows modifying or checking requests and responses. Common
use-cases for this are logging, authentication, and performance monitoring.
Each middleware gets passed a next
function in the context argument that is
used to trigger child handlers. The ctx
also has a state
property that can
be used to pass arbitrary data to downstream (or upstream) handlers. This
state
is included in PageProps
by default, which is available to both the
special _app wrapper and normal
routes. ctx.state
is normally set by modifying its
properties, e.g. ctx.state.loggedIn = true
, but you can also replace the
entire object like ctx.state = { loggedIn: true }
.
import { FreshContext } from "$fresh/server.ts";
interface State {
data: string;
}
export async function handler(
req: Request,
ctx: FreshContext<State>,
) {
ctx.state.data = "myData";
const resp = await ctx.next();
resp.headers.set("server", "fresh server");
return resp;
}
export const handler: Handlers<any, { data: string }> = {
GET(_req, ctx) {
return new Response(`middleware data is ${ctx.state.data}`);
},
};
Middlewares are scoped and can be layered. This means a project can have multiple middlewares, each covering a different set of routes. If multiple middlewares cover a route, they will all be run, in order of specificity (least specific first).
For example, take a project with the following routes:
└── routes
├── _middleware.ts
├── index.ts
└── admin
├── _middleware.ts
└── index.ts
└── signin.ts
For a request to /
the request will flow like this:
- The
routes/_middleware.ts
middleware is invoked. - Calling
ctx.next()
will invoke theroutes/index.ts
handler.
For a request to /admin
the request flows like this:
- The
routes/_middleware.ts
middleware is invoked. - Calling
ctx.next()
will invoke theroutes/admin/_middleware.ts
middleware. - Calling
ctx.next()
will invoke theroutes/admin/index.ts
handler.
For a request to /admin/signin
the request flows like this:
- The
routes/_middleware.ts
middleware is invoked. - Calling
ctx.next()
will invoke theroutes/admin/_middleware.ts
middleware. - Calling
ctx.next()
will invoke theroutes/admin/signin.ts
handler.
A single middleware file can also define multiple middlewares (all for the same route) by exporting an array of handlers instead of a single handler. For example:
export const handler = [
async function middleware1(req, ctx) {
// do something
return ctx.next();
},
async function middleware2(req, ctx) {
// do something
return ctx.next();
},
];
It should be noted that middleware
has access to route parameters. If you’re
running a fictitious routes/[tenant]/admin/_middleware.ts
like this:
import { FreshContext } from "$fresh/server.ts";
export async function handler(_req: Request, ctx: FreshContext) {
const currentTenant = ctx.params.tenant;
//do something with the tenant
const resp = await ctx.next();
return resp;
}
and the request is to mysaas.com/acme/admin/
, then currentTenant
will have
the value of acme
in your middleware.
Middleware Destination
To set the stage for this section, let’s focus on the part of FreshContext
that looks like this:
export interface FreshContext<State = Record<string, unknown>> {
...
next: () => Promise<Response>;
state: State;
destination: router.DestinationKind;
remoteAddr: {
transport: "tcp" | "udp";
hostname: string;
port: number;
};
...
}
and router.DestinationKind
is defined like this:
export type DestinationKind = "internal" | "static" | "route" | "notFound";
This is useful if you want your middleware to only run when a request is headed
for a route
, as opposed to something like http://localhost:8001/favicon.ico
.
Example
Initiate a new Fresh project (deno run -A -r https://fresh.deno.dev/
) and then
create a _middleware.ts
file in the routes
folder like this:
import { FreshContext } from "$fresh/server.ts";
export async function handler(req: Request, ctx: FreshContext) {
console.log(ctx.destination);
console.log(req.url);
const resp = await ctx.next();
return resp;
}
If you start up your server (deno task start
) you’ll see the following:
Task start deno run -A --watch=static/,routes/ dev.ts
Watcher Process started.
The manifest has been generated for 4 routes and 1 islands.
🍋 Fresh ready
Local: http://localhost:8000/
route
http://localhost:8000/
internal
http://localhost:8000/_frsh/js/3c7400558fc00915df88cb181036c0dbf73ab7f5/deserializer.js
internal
http://localhost:8000/_frsh/js/3c7400558fc00915df88cb181036c0dbf73ab7f5/signals.js
internal
http://localhost:8000/_frsh/js/3c7400558fc00915df88cb181036c0dbf73ab7f5/plugin-twind-main.js
internal
http://localhost:8000/_frsh/js/3c7400558fc00915df88cb181036c0dbf73ab7f5/main.js
internal
http://localhost:8000/_frsh/js/3c7400558fc00915df88cb181036c0dbf73ab7f5/island-counter.js
internal
http://localhost:8000/_frsh/refresh.js
static
http://localhost:8000/logo.svg?__frsh_c=3c7400558fc00915df88cb181036c0dbf73ab7f5
internal
http://localhost:8000/_frsh/alive
internal
http://localhost:8000/_frsh/js/3c7400558fc00915df88cb181036c0dbf73ab7f5/chunk-PDMKJVJ5.js
internal
http://localhost:8000/_frsh/js/3c7400558fc00915df88cb181036c0dbf73ab7f5/chunk-UGFDDSOV.js
internal
http://localhost:8000/_frsh/js/3c7400558fc00915df88cb181036c0dbf73ab7f5/chunk-RCK7U3UF.js
That first route
request is for when Fresh
responds with the root level
index.tsx
route. The rest, as you can see, are either internal
or static
requests. You can use ctx.destination
to filter these out if your middleware
is only supposed to deal with routes.
Middleware Redirects
If you want to redirect a request from a middleware, you can do so by returning:
export function handler(req: Request): Response {
return Response.redirect("https://example.com", 307);
}
307
stands for temporary redirect. You can also use 301
for permanent
redirect. You can also redirect to a relative path by doing:
export function handler(req: Request): Response {
return new Response("", {
status: 307,
headers: { Location: "/my/new/relative/path" },
});
}