Murasaki

Middleware

A route guard that runs before every client-side navigation.

src/middleware.ts may export a default function that runs before every navigation — initial mount, push/replace, and browser back/forward — and can redirect it:

src/middleware.ts
import type { Middleware } from 'murasaki'

const middleware: Middleware = ({ pathname }) => {
  if (pathname === '/admin') return { redirect: '/' }
}

export default middleware

<AppRouter> (mounted for you by the scaffold's entry) picks this up automatically from the virtual route table — there's nothing to register by hand, and nothing happens if the file doesn't exist.

The signature

export interface MiddlewareContext {
  pathname: string
}

export type MiddlewareResult = { redirect: string } | void | undefined

export type Middleware = (
  ctx: MiddlewareContext,
) => MiddlewareResult | Promise<MiddlewareResult>

ctx is deliberately small — just the pathname being navigated to. There's no request/response, no headers, no cookies: this runs entirely client-side, inside the same WebView as your routes, not an edge or server runtime. To return early without redirecting, return nothing (undefined).

It may be async — <AppRouter> awaits it before rendering the matched route, rendering nothing in between so the guarded route never flashes on screen.

Guarding a route

The most common shape is an auth check that reads some client-side state (a store, localStorage, a cookie via document.cookie) and redirects unauthenticated visitors away from a protected path:

src/middleware.ts
import type { Middleware } from 'murasaki'
import { useAuth } from '@/lib/auth'

const PROTECTED = ['/settings', '/billing']

const middleware: Middleware = ({ pathname }) => {
  const isProtected = PROTECTED.some((p) => pathname.startsWith(p))
  if (isProtected && !useAuth.getState().isSignedIn) {
    return { redirect: '/login' }
  }
}

export default middleware

Side effects without redirecting

middleware doesn't have to redirect anything — returning void lets the navigation through as-is, so it's also a convenient place to run a side effect on every navigation (analytics page views, for example):

src/middleware.ts
import type { Middleware } from 'murasaki'
import { trackPageview } from '@/lib/analytics'

const middleware: Middleware = ({ pathname }) => {
  trackPageview(pathname)
}

export default middleware

A redirect loop (5+ consecutive redirects) is caught automatically: Murasaki logs a warning and renders the current path as-is rather than hanging.

There's no matcher/config export to scope which paths run middleware — unlike Next.js, middleware always runs on every navigation, and you branch on pathname yourself inside the function (as in the examples above).

This is Murasaki's client-side equivalent of Next.js middleware — there's no edge runtime involved, since it runs in the same WebView as your routes.

Next

Improve this page on GitHub

On this page