Skip to content

Free MIT template · Day 6 of 365

You're looking at a demo. Download the built static files (drop on any host) or grab the full source to customize.

↓ Download static ↓ Source (Astro) All 365 →Build with me →

# 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 moduleasync () => ({ 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>;};

Note

Match order is the route table’s source order. If two routes both match a URL (e.g. `/posts/new` matching both `/posts/:id` and `/posts/new`), put the more specific one first.

## 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.