Docs

Upgrading from Nifra 2.x to 3.0

Nifra 3.0 makes a redirect the same plain-data value an ordinary return is, and freezes the reserved typed-client segment keys into a published contract. The upgrade command handles the deterministic dependency pins; this guide covers the two structural changes it deliberately cannot guess, plus the Node proxy transport default.

1. Run the executable upgrade

Shell
nifra upgrade 3.0.0                 # dry-run: inspect every planned edit
nifra upgrade 3.0.0 --write         # apply edits, then run nifra check
bun install
bun run test
bun run build

The command pins every matching @nifrajs/*, Nifra, and create-nifra dependency to 3.0.0 while preserving caret/tilde/exact style. No package is removed and no import specifier moves this release, so the runner only pins. Dry-run is the default; --write applies and verifies with nifra check.

2. Read redirects as plain data

redirect(...) now returns the same plain render value status(...) produces, not a Web Response. Same bytes on the wire, now with a content-length on Node instead of a chunked empty body. It is still returned or thrown from the same places - loader, action, layout gate - and return redirect() / throw redirect() stay interchangeable, including the client-submit conversion to a 204 with X-Nifra-Redirect.

Breaking: the value is no longer a Response, so .status, .headers, and instanceof Response are gone from it. Read .plain, build one with toResponse() when needed, pass headers through the second argument, and assert on .plain in tests. A hand-rolled Response from a loader or action is untouched - only what redirect() itself returns changed.

TS
// 2.x - a redirect was a Web Response
const r = redirect("/done")
r.status              // 302
r.headers.get("location")
r instanceof Response // true

// 3.0 - a redirect is plain render data
const r = redirect("/done")
r.plain               // { status, headers, body }
r.toResponse()        // build a Response only when something genuinely needs one

// Add headers through the second argument; cookies still ride c.set as before.
redirect("/done", { status: 307, headers: { "cache-control": "no-store" } })

3. Fix reserved typed-client segments

The reserved proxy keys are now a frozen, published contract. A route whose static path segment spells one of them - an HTTP verb in any casing, or subscribe, ws, index, then - is unreachable by property access, because the proxy resolves the reserved key before a path segment. The types now reject that access at compile time instead of silently reaching the wrong node.

TS
// A route whose path segment spells a reserved client key
// (get/post/put/patch/delete/head/options, subscribe/ws/index/then)
// is no longer reachable by property access:
api.posts.delete.get()   // 3.0: type error - `delete` is a reserved verb key

// The typed spelling is a call on the parent node with the segment:
api.posts("delete").get() // reaches /posts/delete

Run nifra fix --code NF-C018 to rewrite the affected call sites. It reads them from the compiler rather than a text search, so it finds every one and never mistakes a real .delete verb call for a path segment; a site it cannot rewrite confidently (bracket access, a node held in a variable) is reported and left alone. nifra routes annotates a colliding route with the spelling that reaches it, in both the table and --json. The set is exported as the one place it is written down.

TS
// doc-check: skip - fragment reads the exported reserved-key contract
import { RESERVED_VERB_KEYS, RESERVED_EXACT_KEYS, reservedKeyFor } from '@nifrajs/client'

reservedKeyFor('delete') // 'delete'
reservedKeyFor('posts')  // undefined

4. Review the Node proxy transport (only if you use it)

On Node, @nifrajs/proxy now defaults to the undici transport, shipped from the new @nifrajs/proxy/undici subpath. Pass an explicit transport to createProxy to override it.

TS
// doc-check: skip - fragment shows the Node transport default and its override
import { createProxy } from '@nifrajs/proxy'
import { undiciTransport } from '@nifrajs/proxy/undici'

// 3.0 on Node: undici is the default transport.
const proxy = createProxy({ target })

// Override it explicitly when a different transport is wanted:
const custom = createProxy({ target, transport: undiciTransport() })

5. Run the release gates

  1. Run nifra check --json and fix every error, including any remaining NF-C018.
  2. If the project has nifra.assurance.ts, run nifra assure --json.
  3. Run the application test suite and a production build.
  4. Exercise every redirect path - loader, action, and layout gate - and, if the app proxies, each deploy adapter it uses.