Docs

Types-first architecture

Nifra starts with automatic inference. A route's path literal, handler context, and return value produce its TypeScript surface; add a Standard Schema when you need runtime validation, coercion, or an explicit request/response contract. The same server type then drives the no-codegen client and the agent-readable API surface.

Inline inference first

The chainable builder is fully type-inferred. Path parameters are parsed from the route pattern, handler context is typed automatically, and plain return values become the success response seen by the client. This is the quick-start style, similar to Elysia's inline route inference.

TS
import { server } from "@nifrajs/core/server"

export const app = server()
  .get("/users/:id", (c) => ({ id: c.params.id, name: "Ada" }))
  // c.params.id is inferred from the path; the return type becomes the client response.
  .post("/users", () => ({ created: true }))

// No schema, annotation, or codegen is required for the route types.

Schemas at the trust boundary

Attach a Standard Schema when inputs must be validated before the handler runs, or when you want an explicit response contract. Path params are already inferred from :id; the params schema below adds runtime constraints/coercion as well.

TS
import { t } from "@nifrajs/schema"

// Add a schema when the boundary needs runtime validation or a declared contract.
export const GetUser = {
  params: t.object({ id: t.string() }),
  response: t.object({
    id: t.string(),
    name: t.string(),
    role: t.union([t.literal("admin"), t.literal("user")]),
  }),
}

Runtime validation

Attach the schema to a route. Path params, query, and body are validated at the runtime boundary before your handler runs - invalid input is rejected with a 422, so the handler only ever sees well-formed data.

TS
import { server } from "@nifrajs/core/server"
import { GetUser } from "./schema"

export const app = server().get("/users/:id", GetUser, (c) => {
  // c.params.id is typed `string` - parsed from the path and validated at the boundary.
  return { id: c.params.id, name: "Ada", role: "admin" as const }
  //     ^ the return is checked against GetUser.response - a wrong shape is a tsc error.
})

Inferred types

Without a schema, c.params.id is still string and the handler return is captured automatically. With a schema, its output types the handler and its declaredresponse constrains what the handler may return. Change either the route or the contract and the typed client follows at compile time.

The typed client

The client is inferred from the server's type - no generators, no build step, no SDK to regenerate. Paths and params autocomplete; the response is typed from the route. A backend change that breaks a call is a compile error on the frontend.

TS
import { client } from "@nifrajs/client"
import type { app } from "./server"   // a TYPE import - server code never ships to the client

const api = client<typeof app>("https://api.example.com")

const res = await api.users({ id: "42" }).get()   // path + params autocomplete, no codegen
if (res.ok) {
  res.data.name        // typed from the route's response schema
} else {
  res.error            // client-call failures are returned, never thrown
}

Contract-first when the surface must be separate

Inline inference is the default. If the API needs to be shared, versioned, or implemented by more than one service, declare it with defineContract and connect handlers with implement. That keeps the contract type available without importing a server implementation into the client.

See Framework contract for the decoupled form and its scaling guidance.

OpenAPI

The openapi() middleware builds an OpenAPI 3.1 document from your registered routes and their schemas - generated lazily on first request, never hand-written. Pass ui: true to also serve a Scalar reference page.

TS
import { server } from "@nifrajs/core/server"
import { openapi } from "@nifrajs/middleware"
import { GetUser } from "./schema"   // the contract defined above

// Generates an OpenAPI 3.1 document from your registered routes - lazily, on first request.
export const app = server()
  .use(openapi({ info: { title: "My API", version: "1.0.0" }, ui: true }))
  .get("/users/:id", GetUser, (c) => ({ id: c.params.id, name: "Ada", role: "admin" as const }))

// → GET /openapi.json   (the spec, generated from your schemas)
// → GET /reference      (a Scalar API-reference page, because `ui: true`)

The MCP contract

The same routes and schemas feed coding agents. nifra context prints the live API surface as compact text, and nifra mcp serves it over the Model Context Protocol so Claude Code or Cursor read the real contract instead of guessing.

Shell
$ nifra context        # the same contract as compact text - pipe into any agent prompt
  GET /users/:id  →  params { id: string }  response { id, name, role }

$ nifra mcp            # the same data over an MCP server - Claude Code & Cursor read it

Known limitations

  • Validation only covers what you put in a schema. A raw-body, file-upload, or bring-your-own-validation route reads the body directly - cap and validate those yourself (see Security's c.boundedBody).
  • The typed client infers from the server type, so it needs an import type of your app and TypeScript on the frontend. There is no runtime coupling - server code never ships to the client.
  • The generated OpenAPI document is a structural subset of 3.1 derived from your schemas; it reflects exactly what the routes declare, not hand-authored prose.