Loaders & Actions
A route loads data with a loader and handles mutations with an action. Both are exported
from the route file alongside the page:
loader: ({ request, params }) => unknown | Promise<unknown>;
action: ({ request, params }) => unknown | Promise<unknown>;
Lifecycle
The order of execution depends on the HTTP method:
- On
POST/PUT/PATCH/DELETE, theactionruns first. - The
loaderruns on every method — including after a successful action. - The page component renders with the results.
The return values flow into the page as props:
| Function | Runs on | Return → prop |
|---|---|---|
loader |
every method | props.data |
action |
mutating methods (POST/PUT/PATCH/DELETE) | props.actionData |
Request access and sessions
The page component never sees request — only loader and action do. Read cookies
and headers there:
import { readSession } from "@/lib/session";
export function loader({ request }) {
const session = readSession(request); // reads the session cookie off request.headers
return { user: session?.user ?? null };
}
readSession(request) reads the session cookie from request.headers. Pass whatever the
page needs down through the return value; do not try to reach request from the component.
Header-read (vary) tracking
The runtime records which header names a loader reads — the "vary set" — and uses it for caching. Read only the headers you actually need. Reading a broad or per-request header (cookies, auth) makes the response vary on it, reducing cache hits; touching nothing keeps the route cacheable.
Short-circuiting the render
Returning or throwing a Response from either function short-circuits the page render
(Remix-style). Use this for redirects and 404s:
import { redirect, notFound } from "@bext-stack/framework";
export function loader({ request, params }) {
const session = readSession(request);
if (!session) throw redirect("/login");
const post = getPost(params.slug);
if (!post) throw notFound();
return { post };
}
- Return a
Responseto send it directly. - Throw a
Responseto abort deeper work and send it.
Error boundary
Uncaught errors are caught by the segment's error.tsx:
/** @jsxImportSource @bext-stack/framework */
export default function Error({ error, reset, route }) {
return (
<div role="alert">
<p>Something went wrong: {String(error)}</p>
<button onClick="location.reload()">Try again</button>
</div>
);
}
reset re-attempts the segment; route describes the failing route.
Full example: page + loader + action
/** @jsxImportSource @bext-stack/framework */
import { redirect } from "@bext-stack/framework";
import { readSession } from "@/lib/session";
import { createPost, listPosts } from "@/lib/posts";
// Runs first on POST, before the loader.
export async function action({ request }) {
const session = readSession(request);
if (!session) throw redirect("/login");
const form = await request.formData();
const title = String(form.get("title") ?? "");
if (!title) return { error: "Title is required" };
await createPost({ authorId: session.user.id, title });
return { ok: true };
}
// Runs on every method (and after a successful action).
export async function loader({ request }) {
const session = readSession(request);
return { posts: await listPosts(), signedIn: Boolean(session) };
}
export default function Page({ data, actionData }) {
return (
<main>
{actionData?.error && <p role="alert">{actionData.error}</p>}
<form method="post">
<input name="title" placeholder="Title" />
<button type="submit">Create</button>
</form>
<ul>{data.posts.map((p) => `<li>${p.title}</li>`).join("")}</ul>
</main>
);
}
API route example (route.ts)
A route.ts handles raw HTTP with one function per method — no page render:
import { json } from "@bext-stack/framework";
import { listPosts, createPost } from "@/lib/posts";
export async function GET(request: Request) {
return json(await listPosts());
}
export async function POST(request: Request) {
const body = await request.json();
const post = await createPost(body);
return json(post, { status: 201 });
}
Where to next
| Page | What it covers |
|---|---|
| Routing & Rendering | Route files, dynamic routes, ISR/SSR, response helpers |
| KV Store | Persist small state a loader can read |
| Entities (Neon) | Config-driven CRUD and direct Neon access |
| Sessions & Tokens | How the session cookie / token is resolved |