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 →Hire me →

# Basic routing

A route is a `path` plus a `handler`. routekit supports three kinds of segments — literal, parameter, and wildcard — and matches them top-down through the route table.

## Path syntax

Segment | Example | Notes
--- | --- | ---
Literal | `/about` | Exact match.
Parameter | `/users/:userId` | Captures one segment into `params.userId`.
Wildcard | `/files/*` | Captures everything to the end into `params.wildcard`.

    const routes = [  { path: '/',                    handler: () => import('./pages/home') },  { path: '/about',               handler: () => import('./pages/about') },  { path: '/posts/:slug',         handler: () => import('./pages/post') },  { path: '/posts/:slug/edit',    handler: () => import('./pages/edit') },  { path: '/files/*',             handler: () => import('./pages/file') },] as const;

## Params

Each `:name` segment becomes a string in `match.params`. Multiple params per route are fine:

    { path: '/orgs/:orgId/teams/:teamId', handler: () => import('./pages/team') }

Params are always strings. If you need a number, parse it in your handler:

    const userId = Number(match.params.userId);if (Number.isNaN(userId)) {  // → not a number; render 404 or redirect}

Tip

routekit deliberately doesn’t coerce types for you. URL parsing rules differ per app — some apps want `'42'` as a string (for opaque IDs); some want `42` as a number. Decide once in your handler.

## Match order

The route table is walked top-to-bottom. The first match wins. This means specific routes go before generic ones:

    // ✅ Correct: '/posts/new' is checked before '/posts/:id'const routes = [  { path: '/posts/new', handler: ... },  { path: '/posts/:id', handler: ... },];
    // ❌ Wrong: '/posts/new' will match '/posts/:id' with id='new'const routes = [  { path: '/posts/:id', handler: ... },  { path: '/posts/new', handler: ... },];

If you find yourself depending on subtle ordering, that’s a smell — see if you can disambiguate by URL shape (e.g. `/posts/new` → `/posts/create`).

## Wildcards

A trailing `*` captures everything else:

    { path: '/files/*', handler: ... }// matches /files/a, /files/a/b, /files/a/b/c.png// match.params.wildcard === 'a/b/c.png'

Wildcards are useful for file browsers, catch-all 404 pages, and proxy-style routes. They cannot appear in the middle of a path (`/files/*/raw` is invalid).

## 404 handling

Add a wildcard route at the end of your table for unmatched URLs:

    const routes = [  // ... real routes ...  { path: '/*', handler: () => import('./pages/404') },] as const;

Without a fallback, an unmatched URL emits a `'no-match'` event instead of `'navigate'`. If you prefer, listen for that explicitly:

    router.on('no-match', ({ url }) => {  console.warn('No route matched', url);});

## Nested routes

routekit doesn’t have nested-route primitives. Layouts are something you compose at the renderer layer — for example, a React app might have an `<AppShell>` that always wraps the matched page component. Keep the routing flat and the layout in your renderer.

If you specifically want a nested-router feel (parent owns layout, children render in an outlet), see code splitting for one common pattern.