Docs

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 dev

To 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 dev

See 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/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. Add the client package when wiring a separate frontend; the full-stack template already includes it.

bun add @nifrajs/client
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<typeof app>("http://localhost:3000")
const { data, error } = await api.users({ id: "7" }).get()
//      ^? { id: string } | undefined

Loaders & 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.

TS
// 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" }
}
TS
// 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/deno adapters.
  • 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.