Getting started
Nifra is a contract-first TypeScript framework for applications with agents in the loop. Start with just a typed backend - like Hono or Elysia - and the client infers its types with zero codegen. Add a full-stack UI, a bounded agent runtime, or page-local WebMCP when you need one: the same contracts drive all of them across Bun, Node, Deno, and the edge.
Start a project
The API starter is the shortest path to a running Nifra app. It includes the server entry, scripts, and a working example:
bun create nifra my-app
cd my-app
bun install
bun run devTo start with a full-stack React app instead, scaffold the site template. It includes the web runtime, renderer, and typed client used by the loader examples below:
bun create nifra my-app --template site --framework react
cd my-app
bun install
bun run devSee the CLI guide for other templates, UI frameworks, and deploy targets.
One app, two agent loops
A coding agent can inspect the live project, change it, run real requests, and prove the result. A user-facing agent can discover explicit page tools, stream progress into the UI, predict a state change, and reconcile it with the server. Both use the same typed capabilities and verification boundary. Start with the agent layer or the WebMCP & predictive UI guide when you are ready to add them.
A backend-only alternative
If you are adding only an API to an existing project, install the core package directly. The scaffold above is not required for a backend-only app.
bun add @nifrajs/coreA 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():
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. Add the client package when wiring a separate frontend; the full-stack template already includes it.
bun add @nifrajs/clientimport { 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<typeof app>("http://localhost:3000")
const { data, error } = await api.users({ id: "7" }).get()
// ^? { id: string } | undefinedLoaders & the full stack
In the site template, a route is two files: the page, and a .backend.ts half whose loader calls your backend in-process during SSR and whose actions handle mutations. Only what the loader's output schema declares reaches the browser. Add streaming, defer(), optimistic UI, and a keyed query cache as you grow. The data model is framework-agnostic, so the renderer stays replaceable. See Project structure for where code lives.
// routes/index.backend.ts - the loader runs on the server (in-process during SSR, no network),
// fully typed against your contract. This file never reaches the browser.
import { t } from "@nifrajs/schema"
import type { Route } from "./+types/index"
// The page receives exactly this shape; anything else the loader returns stays on the server.
export const loaderOutput = t.object({ id: t.string() })
export async function loader({ api }: Route.LoaderArgs) {
const res = await api.users({ id: "7" }).get()
return { id: res.ok ? res.data.id : "unknown" }
}// routes/index.tsx - the page, typed from loaderOutput.
import type { Route } from "./+types/index"
export default function Page({ data }: Route.ComponentProps) {
return <h1>{data.id}</h1>
}Deploy anywhere
- Bun -
app.listen()(native). - Node / Deno - the
@nifrajs/node/@nifrajs/denoadapters. - Cloudflare Workers / Pages - edge build via
buildServer+toFetchHandler(the exact way this self-hosted site is compiled and served).
See the benchmarks for how it performs.