brust 2.0.0-alpha.5
Guide / Routing

Routing

One file, routes.tsx, maps paths to components. Routes nest: a route with children is a layout.

Map paths to components

defineRoutes checks the tree and returns it. These are the pokedex example's routes:

routes.tsx
import { defineRoutes } from '@brust/core/routes'
import AppLayout from './components/AppLayout'
import BrowsePage from './pages/BrowsePage'
import DetailPage from './pages/DetailPage'
import HomePage from './pages/HomePage'
import NotFoundPage from './pages/NotFoundPage'

export const routes = defineRoutes([
  {
    Component: AppLayout,
    children: [
      { path: '/', Component: HomePage },
      { path: '/pokedex', Component: BrowsePage },
      { path: '/pokemon/{name}', Component: DetailPage },
    ],
  },
  { path: '*', Component: NotFoundPage },
])

A route has five fields, and only these five: anything else fails the build.

@brust/core/routes
interface Route {
  path?: string                 // '/pokemon/{name}', or '*' at the root
  Component?: ComponentType
  loader?: (ctx) => data        // see Loaders and params
  cache?: RouteCacheConfig      // see Caching
  children?: Route[]            // makes this route a layout
}

Try a path

The matcher below runs the routes above in the browser: type a path and see which route answers it, with the decoded params.

Live demoRouteMatcher · native
Route
/pokemon/{name}
Params
name = "mr mime"
Status
200

Params and the catch-all

  • A segment in braces, {name}, is a param. The loader gets it decoded: %20 becomes a space and %2F becomes /, while + stays a plus. A value with a broken % sequence is passed raw.
  • '*' is the catch-all. It is allowed once, at the root, and it renders with status 404. Without one, an unknown path gets a plain 404.
  • Two routes that differ only in a param name, such as /a/{id} and /a/{slug}, are the same route: the build fails with duplicate-route.

Paths beyond ASCII

A literal path may contain any character: { path: '/café' } matches what a browser sends, /caf%C3%A9. The build stores the pattern in that encoded form and normalises every request path the same way (lowercase hex is uppercased, raw UTF-8 is encoded) before matching, so /café, /caf%c3%a9 and /caf%C3%A9 are one route and one cache entry. The path is never decoded before matching: %2F inside a param stays a literal slash in the param's value, and a loader still gets the decoded value in params.

Layouts and Outlet

A route with children is a layout. It can leave out path, which adds no prefix. Its component renders <Outlet/> where the matched child goes. The outermost layout returns the whole document.

components/AppLayout.tsx
import { Outlet } from '@brust/core/routes'

export default function AppLayout() {
  return (
    <html lang="en">
      <head>
        <title>Pokédex</title>
      </head>
      <body>
        <nav><a href="/pokedex">Pokédex</a></nav>
        <main>
          <Outlet />
        </main>
      </body>
    </html>
  )
}
Note

A layout must stay static or native. <Outlet/> inside a component that compiles to a React island fails with outlet-in-react, and an Outlet in a component that is not a layout route fails with outlet-outside-layout.

What the build checks

RuleWhen
route-configAn unknown field, a leaf without path or Component, a bad cache.
duplicate-routeTwo routes match the same paths.
nested-catch-all'*' below the root.
duplicate-catch-allMore than one '*'.