Build an SPA
Build an SPA: a worked guide from route registry through to a production build.
Example
Create one route registry, mount it with createSPA, and keep browser-only services behind lifecycle-owned components.
import { createSPA } from '@askrjs/askr/boot';
import { createRouteRegistry, route } from '@askrjs/askr/router';
const registry = createRouteRegistry(() => {
route('/', DashboardPage);
route('/projects/{projectId}', ProjectPage);
});
await createSPA({ root: document.getElementById('app')!, registry });Goal and architecture
A client-only SPA mounts once, owns routing in the browser, and never touches the server beyond static asset delivery and whatever APIs it calls at runtime. You build a registry with `createRouteRegistry(() => { ... })`, export it, and hand it to `createSPA({ root: '#app', registry })` — the registry is an ordinary value, so the path from route declaration to boot is something you can follow through the imports. `@askrjs/vite`'s `askr()` plugin is the only build-time piece you need — there's no server plugin, no `askrServer()` entry, and no document markers to manage.
Implementation
Define routes with `route(path, Component)` or the nested `page()`/`group()`/`index()` helpers inside your registration function, and read the matched params straight from the component's first argument — Askr infers `RoutePathParams<Path>` from the literal path string, so `route('/users/{id}', UserPage)` gives `UserPage` a typed `id: string` without you writing the type by hand. State inside components is `state()`/`derive()`: read through the getter, write through the setter, no proxies. Wire up `<Link>` or `navigate()` for transitions and `Outlet()` in any layout component that needs to render nested routes.
Failure states
Wrap render-time failures in `ErrorBoundary` with a `fallback` — it logs the underlying error and can reset when your app state changes, which matters in an SPA where a bad route param or a malformed API response shouldn't take down the whole shell. Register a `fallback(Component)` route for unmatched paths so users don't hit a blank screen on a typo'd URL. If a route has an `auth` policy and the resolver returns `deny`/`redirect`, make sure you've actually mounted with `auth` options on `createSPA`, or those policies never run.
Verification
Click through every route you registered and confirm the URL bar, browser back/forward, and scroll position all behave — `scrollRestoration` on `SPAConfig` is opt-in, so verify it's doing what you expect rather than assuming a default. Force a render-time error in a leaf component and confirm the nearest `ErrorBoundary` catches it instead of a blank page. Finally confirm the registry you passed to `createSPA` is the one you actually edited — that is an import you can follow, which is why "routes work in dev but not in the built app" is a much rarer failure with an explicit registry than with an ambient one.