@nifrajs/core
v4.0.2
Published
Bun-native, contract-first HTTP framework - the router, server, and route descriptor.
Maintainers
Readme
@nifrajs/core
The Bun-native, contract-first HTTP framework at the heart of nifra: a radix router, a fully type-inferred server, versionable contracts, lifecycle middleware, and production hardening.
The Web-standard app can be wrapped for Vercel Edge with toVercelHandler, Netlify Functions with
toNetlifyHandler, or API Gateway v1/v2 with toLambdaHandler from @nifrajs/core/server. These are
thin event-envelope adapters; application policy, persistence, and platform credentials stay outside
the core.
bun add @nifrajs/coreimport { server } from "@nifrajs/core/server"
const app = server()
.get("/users/:id", (c) => ({ id: c.params.id }))
.post("/users", { body: nameSchema }, (c) => ({ created: c.body.name }))
.listen(3000)
export type App = typeof app // hand this to @nifrajs/client for end-to-end types@nifrajs/core and @nifrajs/core/server expose the same lean common runtime. Optional systems are
available only from their explicit subpaths, so an ordinary HTTP server never evaluates them:
import { defineContract, implement } from "@nifrajs/core/contract"
import { startCausality } from "@nifrajs/core/causality"
import { defineAssurancePolicy } from "@nifrajs/core/assurance"
import { createDataPort, defineDataContract, diffDataContract } from "@nifrajs/core/data"
import { defineChannel, memoryChannelHub } from "@nifrajs/core/channel"- Inline or contract-first. Write routes inline (types inferred from the
builder), or
defineContract(...)+implement(...)for a decoupled, versionable surface - handlers lift over unchanged.
Scaling route tables
Fluent chains are the best fit for a small route surface. TypeScript's type-instantiation budget usually becomes the limiting factor around 95-100 chained routes because every call both infers its handler context and re-threads the growing typed registry. The runtime router is not the limit, and the routes remain fully typed when you compose them as short domain groups:
const listings = server()
.get("/listings", () => ({ ok: true }))
.get("/listings/:id", (c) => ({ id: c.params.id }))
const agents = server().get("/agents/:id", (c) => ({ id: c.params.id }))
const app = server().get("/health", () => ({ ok: true })).merge(listings).merge(agents)To declare routes under a shared path, use group(). The prefix appears in the typed client,
routes(), and OpenAPI. The builder inherits the chain at the call site. Middleware it adds covers
only its own routes, and its onRequest/onResponse hooks run only for requests at or under the
prefix:
const app = server()
.derive(session)
.group("/admin", (admin) =>
admin.beforeHandle(requireAdmin).get("/users", listUsers).get("/users/:id", getUser),
)
.get("/", home) // requireAdmin does not run hereEach group's routes keep the middleware and assurance captured when that group was defined. For a
contract-owned surface, defineContract(...) + implement(...) is the other escape hatch: the
registry is declared as one object type, so it does not grow one fluent-instantiation level per
route. Split groups before the compiler reports TS2589, rather than weakening the route types.
- Validation at the boundary. Per-route
body/queryis any Standard Schema (zod/valibot/arktype, or@nifrajs/schema'st); invalid input is rejected with a structured422before the handler runs. - Lifecycle middleware.
derive/decorateextend the typed context;onRequest/beforeHandle/afterHandle/onResponse/onErrorrun around handlers;use(middleware)applies a bundle. - Portable response observation. The header/body/raw observer methods are opt-in so ordinary
servers stay lean: add
responseObserver()from@nifrajs/core/response-observerbefore callingonResponseHeaders,onResponseBody, oronResponseRaw. Official middleware that uses these tiers installs the compatibility runtime automatically. - Hardening built in.
stop({ drainMs })graceful shutdown (+ opt-in SIGTERM/ SIGINT),requestTimeoutMs(+ctx.signalandctx.budget), a streaming body-size cap, and a redacting structuredLogger. - One request budget.
ctx.budgetcarries the admitted absolute deadline and monotonicremaining()time. An inboundx-nifra-deadlinecan only shortenrequestTimeoutMs/maxInboundDeadlineMs; malformed and expired values fail before the handler.ctx.signalremains the cancellation primitive and aborts at that same effective deadline. - Route assurance. Official auth, CSRF, body-limit, rate-limit, idempotency,
IP-restriction, and security-header modules publish reflection-safe enforcement evidence.
An ordered
AssurancePolicyclassifies every route and fails closed on missing or forbidden evidence without adding work to the request path. - Owned effect execution.
executeCapability()correlates intent and terminal evidence with an opaqueeffectId, records outcomes automatically, and forwards request cancellation. Add order-scopedaroundCapability()policies for async approval/admission; they receive token-only metadata, have bounded timeouts, and must callnext()exactly once before the effect can run. - Durable workflows (opt in).
@nifrajs/core/durable-executionprovides tenant/principal-bound, signed single-use approval resumes; a durable effect journal + reconciliation scanner; and a typed saga state machine with reverse compensation, retry/backoff, and ambiguous-crash detection. Production constructors reject stores that do not declaredurability: "durable". Operational scans use bounded cursor pages throughreconcileEffectsPage()/reconcileSagasPage(). Provider-confirmed manual review uses effect-ID-boundresolveAmbiguity(), followed byresume()orcompensate(). - Production durable adapters.
@nifrajs/core/durable-adapterssuppliesPostgresDurableExecutionAdapter,SQLiteDurableExecutionAdapter, andDurableObjectExecutionAdapter. Each exposes compatibleeffects,approvals,sagas, andleasesstores. RunrunDurableExecutionAdapterConformance()against the deployment backend. - Bounded reconciliation workers.
@nifrajs/core/reconciliation-workerruns effect or saga scans under an atomic lease with durable cursor checkpoints, a finite page budget, bounded handler concurrency, filters, cancellation, and token-only metrics. A worker invocation always terminates. - Rich wire values (opt in).
@nifrajs/core/wireround-trips dates, bigints, maps, sets, binary, shared references, and cycles through JSON transports. Decoding validates every reachable shape, preserves owned__proto__keys without prototype mutation, and enforces configurable node, depth, collection-entry, and decoded-byte limits. - Versioned transport codecs (opt in). Add
.use(transportCodecs(registry))from@nifrajs/core/transport-pluginand configure the typed client'stransportoption with the same registry.@nifrajs/core/transport-codecnegotiates bounded HTTP representations and supplies the same frame/loader adapters for WebSockets and deferred data. ImportrichWireCodec()from@nifrajs/core/transport-codec-rich; the separate subpath keeps rich-wire code out of plain JSON bundles. - Typed data seam (opt in).
@nifrajs/core/datadefines token-only operation contracts,db.read/db.writecapability names, an opaque request-localRlsScope, typed adapter requests, drift snapshots, andcreateDataPort(contract, adapter, { beacon: useCapability }), which emits the operation's capability evidence - derived from the contract, never from the request - before the private adapter runs. It contains no database driver, tenant identity, policy, row values, or durable store; those belong in the adapter layer. - Typed channels (opt in).
@nifrajs/core/channeldefines typed message contracts, bounded subscriptions, cancellation, per-channel resume cursors, bounded local replay, and a process-local in-memory hub for tests. Durable replay, presence, rooms, and multi-instance fan-out remain adapter concerns.
import { defineAssurancePolicy, evaluateRouteAssurance, NIFRA_ASSURANCE } from "@nifrajs/core/assurance"
const policy = defineAssurancePolicy({
rules: [
{ name: "health", match: { paths: ["/health"] }, require: [] },
{ name: "mutation", match: { methods: ["POST", "PUT", "PATCH", "DELETE"] },
require: [NIFRA_ASSURANCE.AUTHENTICATED, NIFRA_ASSURANCE.CSRF] },
{ name: "read", match: { methods: ["GET", "HEAD"] },
require: [NIFRA_ASSURANCE.AUTHENTICATED] },
],
})
evaluateRouteAssurance(app, policy).ok // pure reflection-time evaluationESM-only; requires Bun at runtime. MIT.
Contract enforcement and application scale
The 95-100 figure is a TypeScript compiler limit for one fluent expression, not a runtime route limit. A 300+ route application should use domain groups merged into a small root or a contract-first registry.
response and errors are typed by default but are not runtime enforcement by themselves. Install
responseContract("enforce") before the routes it should cover to validate JSON/status payloads and
prevent extra fields from reaching the wire. warn preserves the original payload during migration.
Raw Response, streams, redirects, and mountFetch remain explicit transport escape hatches and
must be governed by assurance/static review.
For AI agents
Start with LLM.md - this package's contract card (the exports you call + its footguns),
one cheap read instead of the whole corpus. For the wider framework: the repo's
AGENTS.md is the copy-paste quick reference, and
llms-full.txt is the full machine-readable corpus. Run nifra check as the
done-gate, or nifra mcp to give the agent live project tools.
