Loaders & actions
Loaders fetch data on the server; actions mutate it. Both are typed against your contract, and the client never throws — it returns { data, error }.
Loaders
A route's loader runs on the server and calls your backend in-process during SSR — no network hop. Its return type flows to the component as LoaderData.
// A loader runs on the server — in-process during SSR (no network round-trip),
// fully typed against your backend contract.
export async function loader({ api }: LoaderArgs<typeof app>) {
const res = await api.users({ id: "7" }).get()
return { user: res.data } // serialized to the client for hydration
}Actions & revalidation
An action handles the route's POST. After a client-side submit the page's loader revalidates (no full reload); with JS disabled the native form POST re-renders — progressive enhancement, same code.
// An action handles a mutation (a POST). Progressive-enhancement: works with JS
// off (native form POST) and as a client submit (no full reload) with JS on.
export async function action({ api, request }: ActionArgs<typeof app>) {
const form = await request.formData()
await api.users.post({ name: String(form.get("name")) })
return { ok: true } // the loader revalidates automatically
}
export default function Page(props: { data: LoaderData<typeof loader> }) {
return (
<form method="post">
<input name="name" />
<button type="submit">Create</button>
</form>
)
}Content collections
A content collection turns a folder of Markdown into a typed, schema-validated data source — no hand-rolled readdir + frontmatter parsing. defineCollection (from @nifrajs/content/fs) validates each file's frontmatter against a t schema (a typo'd field fails the build, not production) and renders the Markdown body to HTML; all() / get(slug) return fully-typed entries you read in a loader. Framework-agnostic — the rendered html drops into any adapter (React dangerouslySetInnerHTML, Vue v-html, Svelte {@html}).
// content.config.ts — a typed, validated collection over a folder of Markdown.
import { defineCollection } from "@nifrajs/content/fs"
import { t } from "@nifrajs/schema"
export const blog = defineCollection({
dir: "content/blog",
schema: t.object({ title: t.string(), date: t.string(), draft: t.boolean() }),
})
// a loader — typed + validated entries, no manual fs/frontmatter parsing:
export async function loader() {
const posts = (await blog.all()).filter((p) => !p.frontmatter.draft)
return { posts: posts.sort((a, b) => b.frontmatter.date.localeCompare(a.frontmatter.date)) }
}
// posts[0].frontmatter is { title; date; draft } (typed); posts[0].html is the rendered MarkdownFor optimistic UI, concurrent fetchers, and a keyed query cache, the same primitives compose on both React and Solid.