Router Options
All configuration options for createRouter(manifest, options?). Every field is optional. The router works with zero configuration for development, but production deployments typically set at least root and streaming.
RouterOptions
| Option | Type | Default | What It Does |
|---|---|---|---|
root | string | -- | Base URL for resolving module import() paths. Without this, modules resolve relative to the Juice package, not your app. |
basePath | string | '/' | URL prefix for all routes. Use when your app lives at a subpath (e.g., '/docs'). |
streaming | boolean | 'shell' | false | HTML streaming mode. false = wait for full render. 'shell' = stream after shell. true = stream immediately. |
nonce | string | -- | CSP nonce added to all inline scripts (CSS bootstrap, error recovery, React hydration). |
rsc | boolean | false | Vestigial. RSC content negotiation is automatic -- the router always honors Accept: text/x-component regardless of this setting. SPA navigation works without setting this to true. Exists for backward compatibility only. |
csrfProtection | boolean | { allowedOrigins?: string[] } | true | CSRF validation on POST requests. Pass false for public APIs or webhooks. |
mode | 'development' | 'production' | 'production' | Controls error verbosity (stack traces in dev, generic errors in prod), module caching, and HMR injection. |
cache | CacheAdapter | -- | Cross-request cache adapter. Plug in Redis, Cloudflare KV, or caches.default for CDN-level deduplication. |
requestTimeout | number | undefined | Max request time in milliseconds. Exceeding returns 504 Gateway Timeout. Set to 0 to disable. |
assetPrefix | string | '/' | Public URL prefix for client chunk paths. Use when assets are served from a CDN on a different domain. |
clientEntry | string | -- | Path to the client entry module that calls hydrateRoot(). Only needed for custom client entry points. |
hmrUrl | string | '/@vite/client' | Vite HMR WebSocket URL. Only relevant in development mode. |
onBeforeRequest | (req: Request) => Response | void | -- | Hook called before routing. Return a Response to short-circuit (maintenance mode, health checks). |
onNotFound | (req: Request) => Response | 404 text | Hook called when no route matches. Return a custom 404 page. |
onError | (error: unknown, req: Request) => Response | 500 text | Hook for unhandled errors. Thrown Response objects (redirects, 404s) never reach this hook. |
NavigationOptions (@cmj/juice/client)
Options for initNavigation(setPage, options?). These control how SPA navigation behaves after the initial HTML page load.
| Option | Type | Default | Description |
|---|---|---|---|
viewTransitions | boolean | true | Use the View Transitions API for smooth page transitions. When true, page swaps are wrapped in document.startViewTransition(). Falls back to instant swap in browsers that do not support the API. |
prefetch | boolean | false | Enable link prefetching on hover/focus globally. When true, RSC payloads are prefetched when links receive hover or focus events. Individual Link components can override this via their prefetch prop. |
callServer | (actionId: string, args: unknown[]) => Promise<unknown> | built-in | Custom server action RPC handler. The default POSTs to the current URL with x-juice-action-id, then branches on response Content-Type: text/x-component is decoded via createFromFetch (RSC reconciliation), application/json via response.json(). |
Real Examples
Production Deployment (Cloudflare Workers)
createRouter(manifest, {
root: import.meta.url,
streaming: 'shell',
nonce: crypto.randomUUID(),
csrfProtection: {
allowedOrigins: ['https://admin.example.com'],
},
});Why these options: root is required for production module resolution. streaming: 'shell' gives fast TTFB with correct status codes for the shell. nonce makes inline scripts CSP-compliant. CSRF allows an additional admin origin. Note: rsc is not set because RSC negotiation is automatic.
Public API (No CSRF, Custom Errors)
createRouter(manifest, {
root: import.meta.url,
csrfProtection: false,
requestTimeout: 30_000,
onError: (error, req) => {
console.error('[api]', req.url, error);
return Response.json(
{ error: 'Internal server error' },
{ status: 500 }
);
},
onNotFound: (req) => {
return Response.json(
{ error: 'Not found', path: new URL(req.url).pathname },
{ status: 404 }
);
},
});Why these options: CSRF is disabled because this is a public API that uses bearer tokens, not cookies. requestTimeout prevents slow queries from hanging indefinitely. Custom error handlers return JSON instead of HTML.
Maintenance Mode
createRouter(manifest, {
root: import.meta.url,
onBeforeRequest: (req) => {
if (process.env.MAINTENANCE === 'true') {
return new Response('We are performing scheduled maintenance. Back soon.', {
status: 503,
headers: { 'Retry-After': '3600' },
});
}
},
});CDN Asset Prefix
createRouter(manifest, {
root: import.meta.url,
assetPrefix: 'https://cdn.example.com/assets/',
});When to use: when your static assets (JS chunks, CSS) are served from a different domain than your server. The runtime uses this prefix when generating <script> and <link> tags.
Subpath Deployment
// App lives at example.com/docs/
createRouter(manifest, {
root: import.meta.url,
basePath: '/docs',
});When to use: when your app is mounted at a URL subpath behind a reverse proxy. All route patterns are prefixed with the base path.
CacheAdapter Interface
interface CacheAdapter {
get(key: string): unknown | Promise<unknown>;
set(key: string, value: unknown, ttl?: number): void | Promise<void>;
}The adapter is called by Juice when cross-request caching is needed. keyis a string derived from the route pattern and request parameters. ttlis in seconds when provided. Implementations can be synchronous (in-memory LRU) or async (Redis, KV).
// In-memory LRU (for single-process servers)
const cache = new Map();
createRouter(manifest, {
cache: {
get: (key) => cache.get(key),
set: (key, value, ttl) => {
cache.set(key, value);
if (ttl) setTimeout(() => cache.delete(key), ttl * 1000);
},
},
});When NOT to Configure
The zero-config default (createRouter(manifest)) works for development. Do not add options you do not understand. Common mistakes:
- Setting
streaming: truewithout understanding that status codes are always 200. Use'shell'if you want streaming with correct status codes. - Setting
rsc: truethinking it is required for SPA navigation. It is not -- RSC negotiation is automatic via theAcceptheader. This option has no effect. - Setting
requestTimeouttoo low. If your data fetching takes 2 seconds, a 1-second timeout kills every request. Start without a timeout and add one when you know your P99 latency. - Disabling CSRF on a cookie-based app. If your app uses cookies for auth, CSRF protection is essential. Only disable it for token-based APIs.