brust 2.0.0-alpha.5
Guide / Loaders and params

Loaders and params

A loader is an async function on a route. It runs in a Bun worker for each request, and what it returns becomes the props of the route's components.

Write a loader

lib/loaders.ts
import { type LoaderCtx, notFound } from '@brust/core/routes'
import { fetchPokemon } from './pokeapi'

export async function detailLoader({ params }: LoaderCtx<{ name: string }>) {
  const p = await fetchPokemon(params.name)
  if (!p) return notFound({ name: params.name })
  return { name: p.name, types: p.types, art: p.art }
}

// routes.tsx
{ path: '/pokemon/{name}', Component: DetailPage, loader: detailLoader }

What a loader gets

@brust/core/routes
interface LoaderCtx<P> {
  params: P        // decoded route params
  path: string     // the request path
  req: {
    method: string
    url: string
    headers: Record<string, string>
    cookies: Record<string, string>
    search: Record<string, string>   // query, url-decoded, + is a space
  }
}

How the data reaches components

The page component receives the loader's object as its props:

pages/DetailPage.tsx
export default function DetailPage({ name, types, art }) {
  return (
    <article>
      <img src={art} alt={name} />
      <h1>{name}</h1>
      <p>{types.join(' / ')}</p>
    </article>
  )
}

When layouts have loaders too, they run first, root to leaf, and their results are merged into one object that every component in the chain receives. A child's key wins over a parent's. A loader that returns nothing adds nothing.

Note

The merge is flat, so name keys with care. On this site the layout loader returns version for the header; a page loader returning its own version would replace it on that page. Group page data under one key (this page uses demo).

Try a lookup

On a brust server, a loader like the one below reads ?pokemon= from req.search and looks the name up in the site's Pokémon module, and the page is rendered again with the new data. This site is a static export, so the demo runs the same lookup in your browser and rewrites the address.

/docs/loaders?pokemon=pikachuLoaderDemo · native

Pikachu#025

Pikachu

electric

Found in lib/pokemon.ts.

pages/docs/Loaders.tsx
// pages/docs/Loaders.tsx on a brust server (this static site runs the lookup in the browser)
export async function loadersLoader({ req }: LoaderCtx): Promise<LoadersData> {
  const query = (req.search.pokemon ?? 'pikachu').trim().toLowerCase().slice(0, 30)
  const p = byName(query) // lib/pokemon.ts
  return { toc, blocks, demo: { query, found: p !== undefined, card: p ? cardOf(p) : unknownCard(query) } }
}

For a route param, open /demo/pokemon/eevee: its loader reads params.name, and on a brust server an unknown name such as /demo/pokemon/agumon returns notFound(). This site ships as static files and the route's cache rule reads the query string, so the static build leaves it out (ssg: false); run the docs locally with bun run dev to open it.

Not found, redirect, errors

lib/loaders.ts
import { httpError, notFound, redirect } from '@brust/core/routes'

return notFound({ name })            // 404, renders this route's page with the data
return redirect('/pokedex', 308)     // Location header, nothing rendered (default 302)
httpError(429, 'slow down')          // throw-only: any 400-599 status and a body
  • The first verdict stops the chain: loaders below it do not run.
  • notFound(data) renders the route's own page with status 404, its data merged over the parent loaders' data.
  • httpError takes a string body (plain text) or an object (JSON). A thrown Error is a 500, with the message in the server log.
  • A response that carries Set-Cookie is never stored in the page cache.