Patterns · for app developers

The parts bin

I want a page at a URL

Make a directory under app/routes/ matching the path and put a page.tsx in it. That is the whole of it. routes/pricing/ serves /pricing; routes/product/[id]/ serves /product/{id} with params.id.

I want that page to have data

Put a page.loader.ts beside it exporting load. Its return type becomes the page's props type, generated into @generated/client under the route's name.

I want a title and a description

Export meta from the same loader.

ts
export const meta = ({ data }: { data: { name: string } }) => ({
  title: `${data.name} · Storefront`,
  description: `Buy ${data.name}.`,
  head: [og("type", "product"), canonical(`/product/${data.slug}`)],
});

I want a header on every page

routes/layout.tsx with a routes/layout.loader.ts beside it. Keep the loader free of route parameters and the layout survives every navigation beneath it, DOM and island state intact.

I want part of the page to stream

Add loading.tsx beside the page. Its presence is the declaration; nothing in the page or the loader changes.

I want a button that does something

actions.ts beside the page, exporting a declared action. Call it from an island through the generated actions handle. It re-runs the affected loaders on success.

I want to guard a route

Middleware, or a guard at the top of the loader or action. A guard that reads nothing external runs before any socket opens.

I want state the server keeps

A server island: <Island when="visible" mode="server">. The browser ships no component code and every interaction is a round trip.

I want state two islands share

The store. Seed it from a loader's store export, read it with a typed key. Prefix your keys: the store is one flat map.

I want a third-party package

text
fsr add app <name>@<version>

It vendors an ESM bundle under app/vendor/, commits it and adds the import-map entry. Nothing is fetched at deploy time.

I want to call a backend

Declare a client in config/app.toml and put its contract in app/clients/. It arrives in every loader and action as services.<name>, typed from the contract. A transport = "mock" and a .mock.json beside it is how you develop and test against it.

I want to call my own Rust

Mark an impl block and register it. The loader stays where it is; only the computation moves.

rust
#[native]
impl Digest {
  pub fn words(&self, bodies: Vec<String>) -> i64 {
    bodies.iter().map(|b| b.split_whitespace().count() as i64).sum()
  }
}
rust
Host::from(...).native("digest", Arc::new(Digest))
ts
const words = native.digest.words({ bodies });

Rust is synchronous and so is this, so words returns a number rather than a promise. Only what the block declares pub crosses, so a module holding another calls it as an ordinary method and that one never appears in TypeScript. There is no contract, no transport and no interceptor chain, because nothing is crossing a boundary: the declaration comes from the signature, which fsr reads with syn before anything compiles, so the two cannot drift.

Reach for a service instead when the thing genuinely is one, something over a wire that a document already describes, or when you want the cache and the interceptors that come with a call that crosses.

I want to test without a browser

fsr test <app>. Body tests replay a loader against mocked services and assert on what it returned. Page tests render over a DOM. Route tests go through the host. None of them need Node.

I want to know what the host actually did

Read the boot report. Every route with its pattern, every loader with whether it lowered, every component with whether the server can render it, every static root, everything ignored. When something does not work, the report has usually already said so.

I want to see what my TypeScript became

The IR inspector shows real sources beside the exact IR the lowerer produced, including one that is deliberately refused.

Built with SnapFire FSR. Pure Rust runtime, zero Node.js on the server.