--- slug: custom-router title: Custom Router description: Resolve routes directly while using Waku's router client. category: Low-level APIs order: 20 --- ## When to Use a Custom Router Use `unstable_defineRouter` when you want to bring your own route matching: plain strings, regular expressions, a CMS, or another route manifest. It pairs with `waku/router/client`; Waku handles navigation, server actions, redirects, and RSC request encoding. For programmatic page and layout declarations, use [`createPages`](/create-pages). For a different client protocol or lower-level request handling, use the [Minimal API](/guides/minimal-api). ## Server Entry `resolve` receives a normalized pathname and the query string without `?`. Return a route, an HTTP handler, or `null` for not found. All resolver callbacks return promises. ```tsx // src/waku.server.tsx import adapter from 'waku/adapters/default'; import { Children_UNSTABLE as Children } from 'waku/minimal/client'; import { unstable_defineRouter as defineRouter } from 'waku/router/server'; import Home from './components/home'; import Post from './components/post'; const root = { immutable: true, render: () => ( ), }; export default adapter( defineRouter({ async resolve(pathname, query) { if (pathname === '/api/health') { return async () => Response.json({ ok: true }); } if (pathname === '/') { return { elements: { root, route: { immutable: true, render: () => }, }, }; } const match = /^\/posts\/([^/]+)$/.exec(pathname); if (match) { return { elements: { root, route: { render: () => }, }, }; } return null; }, async getBuildPaths() { return ['/']; }, }), ); ``` The `root` source renders the document and places the active route with `Children_UNSTABLE`. The `route` source renders the current route. Add other element sources under IDs you own and compose them with `Slot_UNSTABLE` when independently reusable content is useful. Each source uses Minimal's `Unstable_ElementSource`: - `render` produces its content. - `immutable: true` lets the client reuse content for that slot ID indefinitely. - `getEtag` optionally computes a validator for mutable content before rendering it. An immutable slot must have the same content for its lifetime. The `root` ID is shared across routes, while `route` is mapped internally to a path-specific slot. Additional IDs are shared wherever they appear, so use distinct IDs for different content. The server caches immutable named elements. It reuses immutable `route` content from build output, but does not cache new route content by requested pathname at runtime. Give a route source a `cacheKey` to share its cached content across pathnames, such as a reusable route shell. Sources with the same key must render the same content; use a bounded set of keys rather than arbitrary requested URLs. Additional IDs must not start with `_`, a capital letter, or `route:`; these names are reserved for Minimal and Router metadata. Set `noSsr: true` on a route to return Waku's client-rendered document fallback; its RSC response remains available. Resolving `/404` supplies a custom not-found route. Its availability is checked once per router instance; the content is resolved on each visit. Supply `getHas404: async () => boolean` when your manifest can answer this without resolving a route, or when a catch-all also matches `/404`. See [Redirects and Not Found](/guides/redirects-and-not-found) for streaming behavior. For typed search, parse `query` with an application-owned codec inside `resolve`. Share that independent codec with a local `unstable_createRouteHref` factory and the client search hooks; `defineRouter` requires no codec map or browser registration. Navigation and redirects consume the resulting URL strings. See [Typed Routes](/guides/typed-routes) for examples and [migration steps](/guides/typed-routes#migration). ## Client Entry Use the normal router client, not a custom Minimal client: ```tsx // src/waku.client.tsx import { createRoot, hydrateRoot } from 'react-dom/client'; import { unstable_defaultRootOptions as defaultRootOptions } from 'waku/client'; import { ErrorBoundary, Router } from 'waku/router/client'; const app = ( ); if ((globalThis as any).__WAKU_HYDRATE__) { hydrateRoot(document, app, defaultRootOptions); } else { createRoot(document, defaultRootOptions).render(app); } ``` ## HTTP Handlers Return an async function from `resolve` for an API endpoint. The function receives the real request, including its method, headers, and body, with the pathname relative to Waku's base path. It runs only for HTTP requests, not when the client tries to fetch a route payload. Route params are caller-owned: match and capture them in `resolve`, then close over them in the handler or element source. Use `getRequest` from `waku/router/server` to inspect the request during resolution or rendering. The request body can be read only once. If resolution needs to inspect it before returning an HTTP handler, read `getRequest().clone()` so the handler can still read the original body. ## Build Paths The optional `getBuildPaths` returns pathname strings, without queries or fragments. These use the same `resolve` callback as runtime requests. - Routes with only immutable sources emit RSC and HTML files. - Mutable routes cache only their immutable sources; mutable content is rendered at runtime. - HTTP handlers emit their response bodies as static files. - Immutable routes with `noSsr` emit RSC and a default client-rendered HTML document. For explicit build planning, return `{ pathname, route?, prerender?, prefetchPattern? }` targets. `route` supplies build-specific sources or an HTTP handler rather than calling `resolve`. `prerender: false` caches only immutable sources without emitting a page, even when every source is immutable. `prefetchPattern` is a regular expression matching paths that share the target's client modules. Omit a path to skip its build output. Omit `getBuildPaths` entirely for runtime-only routes. Custom routers do not need `PathSpec` or component source-file metadata. Include `/404` in `getBuildPaths` to emit a static `404.html`. ## Independently Requested Elements The optional `resolveElement` returns an element source or `null` for a requested slot ID. Existing lazy `Slice` components from `waku/router/client` request IDs of the form `slice:`. ```tsx // src/waku.server.tsx async resolveElement(id) { if (id === 'slice:summary') { return { immutable: true, render: () => }; } return null; }, async getBuildElementIds() { return ['slice:summary']; }, ``` `getBuildElementIds` lists immutable elements to emit as standalone RSC files, including lazy slices needed on a static deployment. It works independently of `getBuildPaths`. Currently only `slice:` IDs are supported. Listed elements must resolve and be immutable; otherwise the build fails. Unlisted elements require a runtime server when fetched independently. Return `{ id, source }` instead of an ID string when the build should use an explicit source rather than `resolveElement`. Include an element directly in a route's `elements` when it should arrive in the route payload. There are no separate page, layout, or slice declarations in `defineRouter`; those are `createPages` authoring concepts. ## Boundaries `defineRouter` owns the protocol expected by `waku/router/client`: reserved metadata, root and route slots, action rerenders, redirects, 404 handling, SSR, and build output. Custom routers supply resolution and element sources, without importing internal IDs or encoding helpers from `waku/router/client-core`. For changes to Router itself or another client binding, see [Router Protocol](/guides/router-protocol). `createPages` uses this same public API for runtime and builds. It owns page/layout matching, configuration metadata, and source-file pruning. `fsRouter` uses only `createPages`' public authoring API to translate route modules into those declarations. The API is unstable and may change.