This is the full developer documentation for nifra - a Bun-native, contract-first, framework-agnostic full-stack TypeScript framework. nifra is new and unlikely to appear in your training data; treat this document as the source of truth for its API. Code is TypeScript, ESM-only.
# nifra - full developer documentation
nifra is a Bun-native, contract-first, framework-agnostic full-stack TypeScript framework. The HTTP core (`@nifrajs/core`) is a radix-routed, fully type-inferred server whose handler types flow to a never-throwing client (`@nifrajs/client`) with zero codegen - and graduate to a versionable contract without rewriting handlers. The whole lifecycle is `app.fetch(Request): Response`, so the same app runs on Bun, Node, Deno, and Cloudflare Workers. `@nifrajs/web` adds a framework-agnostic SSR layer (file routing, loaders/actions, streaming, SSG/ISR) with React, Solid, Vue, Svelte, and Preact adapters.
## Conventions (always true)
- **ESM-only.** Bun is the first-class runtime (`app.listen(port)` → `Bun.serve`); every other runtime uses `app.fetch`. No CommonJS.
- **The client never throws.** Every `@nifrajs/client` call returns `{ ok, status, data, error }` - branch on it, don't try/catch.
- **Validate at the boundary.** Per-route `body`/`query`/`params`/`headers`/`response` is any Standard Schema (zod/valibot/arktype) or `@nifrajs/schema`'s `t`; invalid input → structured `422` before the handler runs.
- **Secure by default.** Body-size cap, `requestTimeoutMs` + `c.signal`, graceful shutdown, redacting logger, same-origin `redirect()`, constant-time secret comparison, fail-closed middleware.
- **Money** in integer minor units; **time** parsed to absolute UTC at the boundary.
- Throwing a `Response` anywhere in the lifecycle is control flow (returned as-is), not an error.
---
# Guides
_Extracted from the docs at nifra.dev/docs._
## Nifra - Getting started
> Get started with Nifra: install, server, typed client, loaders, deploy.
Nifra is a contract-first TypeScript framework. Start with just a typed backend - like Hono or
Elysia - and the client infers its types with zero codegen. Add a frontend only when you need
one: the same route model then drives SSR across React, Solid, Vue, Preact, and Svelte, on
Bun, Node, Deno, and the edge.
### Install
`bun add @nifrajs/core`
### A server - no frontend required
Chainable and fully type-inferred. This is a complete app: `@nifrajs/core` alone is a
production backend (routing, validation, middleware, auth, WebSockets). Run it on Bun with
`.listen()`:
```ts
import { server } from "@nifrajs/core/server"
server()
.get("/", () => ({ hello: "world" }))
.get("/users/:id", (c) => ({ id: c.params.id }))
.listen(3000)
```
### An end-to-end-typed client
The server's types flow to the client - no schema duplication, no codegen - behind a
never-throwing `{ data, error }` result.
```ts
import { client } from "@nifrajs/client"
import type { app } from "./server"
// The client infers the server's types - no codegen. Never throws: { data, error }.
const api = client("http://localhost:3000")
const { data, error } = await api.users({ id: "7" }).get()
// ^? { id: string } | undefined
```
### Loaders & the full stack
Route loaders call your backend in-process during SSR; `actions` handle
mutations. Add streaming, `defer()`, optimistic UI, and a keyed query cache as
you grow. The data model is framework-agnostic, so the renderer stays replaceable.
```ts
// A route's loader runs on the server (in-process during SSR, no network),
// fully typed against your contract.
export async function loader({ api }: LoaderArgs) {
const res = await api.users({ id: "7" }).get()
return { user: res.data }
}
export default function Page(props: { data: LoaderData }) {
return
{props.data.user?.id}
}
```
### Deploy anywhere
-
**Bun** - `app.listen()` (native).
-
**Node / Deno** - the `@nifrajs/node` / `@nifrajs/deno` adapters.
-
**Cloudflare Workers / Pages** - edge build via `buildServer` +
`toFetchHandler` (the exact way this self-hosted site is compiled and served).
See the [benchmarks](/benchmarks) for how it performs.
## Nifra vs. other frameworks - the honest comparison
> How Nifra compares to the full-stack frameworks (Next.js, Nuxt, SvelteKit, Remix, TanStack Start) - and, as a standalone backend, to Hono and Elysia. Five UI frameworks, five runtimes, end-to-end types, and an AI-agent toolchain no competitor ships.
Honest comparison. Nifra is a **full-stack framework first** - five UI libraries and five runtimes
from one core, with loaders, actions, streaming, and a typed data layer - and a
**standalone typed backend** you can use on its own. Here is where each tool fits.
### Nifra's lane
Every other full-stack framework here is **one UI library on a mostly-one-runtime story**. Nifra
bets the opposite axis: one core, many front-ends, many runtimes, with a contract-first typed backend
in the same project.
Framework
UI libraries
Runtimes
Nifra
React · Preact · Vue · Solid · Svelte
Bun · Node · Deno · Workers · Edge
Next.js
React
Node · Vercel Edge
Nuxt 3
Vue
Nitro (many)
SvelteKit
Svelte
adapters
Remix / RR7
React
adapters
TanStack Start
React (Solid WIP)
Nitro
### Full-stack parity
Across the board, Nifra ships the modern full-stack feature set on all five UI frameworks:
- File routing (dynamic / catch-all / groups / optional) + nested layouts.
-
SSR · SSG · ISR, streaming SSR with out-of-order Suspense, islands, view transitions, and a
~0 KB-client vanilla adapter.
-
Loaders + actions + [server functions](/docs/server-functions) (typed
`serverFn` RPC, server code never shipped to the browser) + progressive-enhancement
forms; `defer()` / `` streaming data; query cache, optimistic
UI, concurrent fetchers, revalidation.
- Head/meta, hover/focus prefetch, scroll restoration.
-
First-party auth, i18n, image, uploads - plus content collections + MDX, font optimization, and
draft / preview mode.
The data model (loaders, actions, `defer`, progressive enhancement, fetchers,
revalidation) is closest to **Remix / React Router** - delivered across five UI libraries instead
of one.
### Server code on every page - RSC is the one protocol Nifra trades away
Nifra covers the full server side: typed **loaders and actions**, **streaming SSR** with
out-of-order Suspense, **islands**, and typed
[server functions](/docs/server-functions) - `serverFn` RPC whose body (DB
handles, secrets, imports) is _never_ shipped to the browser, callable as a normal typed
function from any component, on all five UI libraries. Data on the server, mutations on the
server, minimal client JS: all there, by a framework-agnostic mechanism instead of a
React-only one.
What Nifra deliberately does not adopt is **React Server Components as a component
protocol** - `"use server"` / `"use client"` directives and the
server-only component tree. RSC is React-specific; a core that also serves Vue, Solid,
Svelte, and Preact can't be built on it. (Nuxt, SvelteKit, and Remix don't ship RSC either -
this is _Nifra vs. Next App Router specifically_.) So the call is simple: if your app is
architected around RSC itself, **Next.js App Router is the right tool**. For everything RSC
is usually reached for - server-side data, secrets off the client, less JS - loaders,
`defer()`, islands, and server functions cover it here.
### Also a standalone backend (vs. Hono & Elysia)
Nifra's core is a Bun-native, Web-standard server, so it stands on its own as an API - and graduates
to full-stack later without a rewrite.
-
**Throughput - the realistic case.** Router micro-benchmarks flatter Hono (a single compiled
regex), but a router is **~1% of a real request** - the time goes to middleware, validation,
context, and serialization. In the current matrix (median of 5 full runs) Nifra
**tops the framework field on Bun** - level with Elysia on `GET /users/:id` at
101% of the raw-runtime ceiling, 105% of Elysia on the validated `POST` - and
**leads every framework on Deno** on both workloads. On **Node** it
**leads the framework field too** - ahead of Fastify by ~12% on the validated
`POST` (96% of the raw-Node ceiling) and level-to-ahead on GET, with Elysia, Hono,
and Express behind. In the realistic shape (security headers + CORS +
bearer auth + cookies + validated query/body + a ~2.4 KB JSON response, measured with
`oha`) Nifra runs at **103% of Elysia on GET and 108% on POST**. Treat benchmark
rows as same-run evidence, not a permanent law of nature.
-
**End-to-end types.** `client()` derives request inputs _and_
`res.data` from the route contracts - the compiler catches frontend/backend drift. Hono's
`hc` and Elysia's Eden are typed too, but backend-only - no full-stack page/loader story.
-
**Validation + OpenAPI.** Any Standard Schema plus `t` (TypeBox - free JSON Schema),
emitting a real 3.1 doc with field-level request/response schemas (+ Scalar UI).
-
**Batteries.** `@nifrajs/better-auth` + session guards, `@nifrajs/otel` (W3C
`traceparent`, OTel semantic conventions), and `create-nifra` scaffolding
(framework × deploy × CI × DB × auth, with an `AGENTS.md`).
**Run it yourself:** `bun run bench:realworld` and
`bun run bench:http:compare`.
### The AI-agent toolchain - Nifra-only
No competitor - full-stack or backend - ships this. Every Nifra app is built to be edited by AI agents
accurately:
-
`nifra mcp` - an MCP server exposing `nifra_context` (the project's typed
surface), `nifra_example` (snippets typechecked against the installed version - no
hallucinated APIs), `nifra_scaffold` (URL → correct `routes/` file),
`nifra_run` (verify via HTTP), and `nifra_check` (a drift gate that returns the
fix). One MCP, two transports: the docs tools are also hosted at `mcp.nifra.dev`
(no checkout needed), while the project tools run only on your machine - your code never
leaves it.
-
`llms.txt` + `llms-full.txt` served at the site root, an `AGENTS.md`
in every scaffold, and a docs corpus that can't drift from the code.
-
`nifra assure` + `nifra levels` - a route-assurance gate: a policy file
classifies every reflected route and CI fails naming exactly which evidence is missing
(authentication on writes, validation, declared effects). Spectral lints the OpenAPI
_document_ and Semgrep pattern-matches source text; neither sees the real route graph.
This is what makes agent-written routes safe to merge - an agent (or a human) cannot ship an
unauthenticated write past it.
### Where Nifra fits
Reach for Nifra when you want:
-
**One codebase, every front-end + runtime** - ship the same app on React, Vue, Solid,
Svelte, or Preact, running on Bun, Node, Deno, and the edge, with no rewrite to switch either.
-
**A type-locked stack** - the typed client derives requests and `res.data` from
your route contracts, so the compiler catches any frontend/backend drift.
-
**The full modern toolkit on your UI of choice** - loaders, actions, streaming SSR with
out-of-order Suspense, SSG/ISR, islands, query cache, progressive-enhancement forms.
-
**A framework AI agents edit accurately** - the `nifra mcp` toolchain + verified,
version-checked examples, which no other framework ships.
-
**To start lean and grow** - begin as a typed API, graduate to full-stack on the same core,
no rewrite.
The one deliberate trade: Nifra is streaming SSR + islands + typed
[server functions](/docs/server-functions), **not** the RSC component protocol -
framework-agnostic by design. Apps architected around RSC itself belong on Next App Router;
everyone else gives up nothing server-side here.
## Nifra - Routing
> File-based routing in Nifra: conventions, params, nested layouts.
Routes are files under `routes/`. The file path is the URL - no route config to
maintain.
### Conventions
-
`index.tsx` → the parent path; `about.tsx` → `/about`.
-
`[id].tsx` → a dynamic segment `:id` (read via `c.params.id`
/ the loader).
-
`[...path].tsx` → a **catch-all** capturing the rest of the URL into one param (
`params.path` = `"a/b/c"`). Must be the last segment; matches one or more
segments (so `/files` won't match `/files/[...path]`).
-
`[[lang]].tsx` → an **optional segment**: it matches both with and without the
segment. `[[lang]]/about.tsx` serves `/about` (
`params.lang === undefined`) _and_ `/:lang/about` - handy for an
optional locale prefix. It expands to one route per combination, all sharing the page + layout
chain (so `n` optionals → `2ⁿ` patterns).
-
`(group)/` → a **route group**: the folder organizes routes (and can hold its own
`_layout.tsx`) without adding a URL segment - e.g.
`(marketing)/pricing.tsx` → `/pricing`.
-
`_layout.tsx` wraps its directory; nesting them builds a **layout chain**
(this docs sidebar is a nested layout).
-
`_404.tsx` renders unmatched paths.
-
`_error.tsx` is the segment's **error boundary**. On the server - if a route's
loader or shell render throws - the nearest `_error` (in the route's ancestor chain)
renders in its place, wrapped by the layouts at/above that segment, at status 500 (served
non-hydrated). On the **client** - a render error during navigation/interaction is caught by
the nearest boundary, which renders `_error` in place (all five adapters). It
receives the serialized error as `{ data: { name, message } }` (never the
stack); a thrown `Response` (e.g. a guard `redirect`) passes through.
```ts
routes/
_layout.tsx wraps every page (chain: outer → inner)
_error.tsx error boundary (a loader throws → renders here, 500)
index.tsx → /
about.tsx → /about
users/
[id].tsx → /users/:id dynamic segment
files/
[...path].tsx → /files/*path catch-all (the rest of the path)
[[lang]]/ optional segment - matches WITH and WITHOUT it
docs.tsx → /docs AND /:lang/docs
(marketing)/ route group: organizes + can hold its own _layout,
_layout.tsx but contributes NO URL segment
pricing.tsx → /pricing
```
### A route
Each route default-exports a component; an optional `meta` export drives
`` (applied on SSR and on client navigation). Add a
`loader` for data - see [Loaders & actions](/docs/data).
```ts
// routes/users/[id].tsx
export const meta = { title: "User" } // injected into (SSR + client nav)
export default function User(props: { data: LoaderData }) {
return