PRISM Data Conventions
PRISM routes are bext components — pure (props) => string (or
Promise<string> for async pages). They render server-side via
@bext-stack/framework's h() runtime; there is no React, no
virtual DOM, no client-side lifecycle. See PRISM
for the framework overview.
For pages that need server-side data fetching or mutation handling,
PRISM offers Remix-style loader and action conventions —
co-locate the data handler with the component in the same route
file, and PRISM calls it on the server before the component runs.
Anatomy of a data route
// src/app/posts/[id]/page.tsx
export async function loader({ request, params }: LoaderArgs) {
const post = await db.post.findUnique({ where: { id: params.id } });
if (!post) throw new Response("Not Found", { status: 404 });
return { post };
}
export async function action({ request, params }: ActionArgs) {
const form = await request.formData();
await db.post.update({
where: { id: params.id },
data: { title: form.get("title") as string },
});
return { ok: true };
}
export default function PostPage({ data, actionData }: PageProps) {
return (
<article>
<h1>{data.post.title}</h1>
{actionData?.ok && <p>Saved.</p>}
<form method="post">
<input name="title" defaultValue={data.post.title} />
<button type="submit">Save</button>
</form>
</article>
);
}
Three exports from the same module:
loader— runs forGET/HEAD. Return value is passed asprops.datato the component. -action— runs forPOST/PUT/DELETE/PATCH. Return value is passed asprops.actionData. PRISM re-runsloaderafter a successful action so the page renders with fresh data, matching Remix semantics. -default— the bext component (returnsstringorAsyncIterable<string>).
Dispatch rules
| Method | Has loader | Has action | What runs |
|---|---|---|---|
GET / HEAD |
yes | any | loader → component |
GET / HEAD |
no | any | component only |
| Non-GET | any | yes | action → loader → component |
| Non-GET | any | no | component only |
The decision happens in Rust, before the render pool is engaged —
see PrismDataCall::for_request in
crates/bext-core/src/framework/prism_data.rs. If the route
exports neither handler, PRISM takes the same no-data code path
it always has; the convention is strictly additive.
Detecting exports
PRISM scans route source files at build time for four canonical export shapes:
export async function loader(...) {}
export function loader(...) {}
export const loader = ...
export { loader }
…and the same four for action. The detector is intentionally
source-level — no ESM parser, just substring matches — because
route files are small and false positives are harmless (the
runtime falls through when the export doesn't actually exist).
One known false-negative: export { something as loader }. If
you rename an identifier inside an export list, the detector
won't notice. Use one of the four canonical shapes above.
Throwing Responses
Either handler can throw new Response(...) to short-circuit
rendering. PRISM catches the thrown response and returns it
verbatim. Use this for 404s, redirects, and auth gates without
polluting component code:
export async function loader({ request }: LoaderArgs) {
const session = await getSession(request);
if (!session.user) {
throw new Response(null, {
status: 302,
headers: { Location: "/login" },
});
}
return { user: session.user };
}
TypeScript
PRISM exports LoaderArgs, ActionArgs, and PageProps from
@bext-stack/framework. The PageProps generic infers data and
actionData from the sibling loader / action return types,
so you get end-to-end inference without explicit annotations:
export async function loader({ params }: LoaderArgs) {
return { post: await load(params.id) };
}
export default function Page({ data }: PageProps<typeof loader>) {
// data.post inferred
}
Caveats
- Not yet wired into the render pool. As of E7, the detection
and dispatch logic are live in
bext-corewith unit coverage, but the V8 / V8 render path still calls the component directly. Wiring the intermediateloader/actionhop into the render pool lands in the E7 follow-up — same cadence as the SolidStart subprocess dispatch. - Runtime-returned JSON is serialised viaJSON.stringify.Date,BigInt, and class instances will lose their types. Superjson support is on the roadmap. - No nested routing yet. In Remix, parent routes get their ownloader. PRISM's router is flat for now; parent-route loaders arrive when PRISM grows nested layouts beyond the current single-layout model.
Related docs
- PRISM overview — the engine itself
- Routing — how src/app/ maps to URLs
- SolidStart — same conventions, different framework