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:
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:
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 middlewareSide 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):
import type { Middleware } from 'murasaki'
import { trackPageview } from '@/lib/analytics'
const middleware: Middleware = ({ pathname }) => {
trackPageview(pathname)
}
export default middlewareA 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.