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
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
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:
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.
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.
Pikachu
Found in lib/pokemon.ts.
// 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
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.httpErrortakes a string body (plain text) or an object (JSON). A thrownErroris a 500, with the message in the server log.- A response that carries
Set-Cookieis never stored in the page cache.