Skip to content

Your first route

This page is the long-form version of the quickstart. Read this once; you won’t need to read it again.

Defining a route

A route is an object with a path and a handler:

{ path: '/users/:userId', handler: () => import('./pages/user') }

The path is a string with optional :param placeholders. The handler is any function that returns a promise — typically a dynamic import, but it can be anything.

// All of these are valid handlers:
() => import('./pages/user') // lazy-loaded module
async () => ({ default: render }) // inline async
() => fetch('/api/page').then((r) => r.text()) // fetch raw HTML

routekit doesn’t constrain what the handler returns. It only calls it and emits the resolved value as part of the match.

Matching at runtime

When the browser URL changes (back button, forward button, link click, router.go(...)), routekit walks the route table top-to-bottom and matches the first one that fits. The match object has shape:

type Match = {
path: string; // the matched route path: '/users/:userId'
url: string; // the actual URL: '/users/42'
params: Record<string, string>; // typed: { userId: '42' }
handler: () => Promise<unknown>;
};

Rendering

routekit emits a navigate event on every URL change. Your app subscribes:

router.on('navigate', async ({ match }) => {
// Resolve the handler — typically a code-split chunk.
const mod = await match.handler();
// Hand off to your renderer. routekit doesn't render anything itself.
render(mod, match.params);
});

This is the entire integration surface with your UI framework. Whether you use React, Preact, Solid, or vanilla DOM, the contract is the same: routekit emits, your renderer renders.

Type-safe params

The static as const on your route table is what makes params typed:

const routes = [
{ path: '/users/:userId', handler: ... },
{ path: '/posts/:slug/comments/:c', handler: ... },
] as const;
const router = createRouter(routes);
router.on('navigate', ({ match }) => {
// match.params is `Record<string, string>` until you narrow on `match.path`.
if (match.path === '/users/:userId') {
match.params.userId; // ← type: string
match.params.foo; // ← type error: no such param
}
});

Without as const, paths erase to string and you lose param inference. See TypeScript usage for the full story including generic helpers.

Cleanup

const off = router.on('navigate', handler);
// later, if you ever need to:
off();

The router itself never needs to be torn down — it lives for the life of the page. The on() return value lets you remove individual listeners (e.g. for hot-reload).

What you have now

A typed, code-split, framework-agnostic router in roughly 30 lines of your code and 3 KB of routekit. Most apps won’t outgrow this — but when you do, the Guides cover nested routes, programmatic navigation, and lazy-loading patterns.