Upgrading from Nifra 3.x or 4.x to 5.0
Nifra 4.0 split every app into a frontend and a backend half, and 5.0 enforces the split and changes a handful of defaults. The upgrade command pins the packages and moves imports; the layout migration moves files; this guide covers what each leaves to you and how to tell a finished migration from one that needs a decision.
1. Run the executable upgrade
bunx @nifrajs/cli@5.0.0 upgrade 5.0.0 # dry run: every release step from the installed version
bunx @nifrajs/cli@5.0.0 upgrade 5.0.0 --write # apply the pins and import moves
bun installRun the command from the release you are moving to. It chains every release recipe between the installed version and 5.0, so a 3.x app takes the 4.0 steps (the retired @nifrajs/web/server-only import moves to @nifrajs/web/backend-only) and the 5.0 ones in one pass, and prints each release's notes. It lists every @nifra-gate-reviewed comment, which silences nothing in 5.0: after review, write // nifra-expect <code>: <reason> with the code nifra check reports there. The upgrade never writes a reason for you.
2. Move onto the frontend/backend layout (from 3.x)
nifra migrate layout # dry run: splits, moves, rewritten imports, issues
nifra migrate layout --write # apply it
nifra schemas --write # draft the output schemas it lists as follow-ups
nifra checkEach route file splits into x.tsx, the page, and x.backend.ts, which holds loader, action, middleware and the other server exports. The root backend.ts and framework.ts move to backend/app.ts and backend/framework.ts, and every other module goes to frontend/, backend/ or shared/ by who imports it at runtime. A folder whose imported files all land in one zone moves whole, so a component nothing imports yet travels with its folder; a folder split between zones is reported instead.
// doc-check: skip - a route split into its two halves
// routes/notes.tsx - the page, shipped to the browser whole
import type { loader } from "./notes.backend.ts"
export default function Notes({ data }: { data: Awaited<ReturnType<typeof loader>> }) {
return <ul>{data.notes.map((note) => <li key={note.id}>{note.title}</li>)}</ul>
}
// routes/notes.backend.ts - loader, action, middleware and the other server exports
import { t } from "@nifrajs/schema"
export const loaderOutput = t.object({
notes: t.array(t.object({ id: t.string(), title: t.string() })),
})
export async function loader() {
return { notes: await listNotes() }
}To keep a folder where it is, declare it a zone before migrating. The migration leaves zoned folders in place, and builds, nifra dev and nifra check enforce them like the default folders.
{
"nifra": {
"zones": {
"frontend": ["src/ui"],
"backend": ["server"]
}
}
}nifra migrate layout --zones writes those entries for you: every module the layout would move stays where it is, and package.json lists the widest folder whose files all belong to one side, or the file itself where its folder holds both. Route files are still split and the root entries still move. A path nested in another's folder overrides it, so a backend file can sit in a frontend folder without moving.
The command exits 0 when nothing needs a decision. A step with a known command, such as drafting an output schema for a backend half it did not write, is listed under next and in followUps with --json, and does not fail it. An issue (an import it could not place, a frontend that still uses a moved name at runtime) exits 1.
5.0 refuses a route file that still exports a server name: nifra build and the server stop on it, and nifra check reports it as NF-C041. nifra fix --code NF-C041 moves the export and the code only it uses into the route's backend half.
3. Draft and review output schemas
A loader or action that returns data needs loaderOutput or actionOutput; the data is projected through it, so undeclared keys never reach the page. nifra schemas --write drafts each missing one from the function's return type, leaves out credential-like fields and marks it nifra:generated so later runs keep it current. A value typed any or unknown drafts as t.json(), which admits anything: nifra schemas lists those paths and nifra check warns on them (NF-C030). Give the value a type and draft again. nifra schemas --check fails on a missing, stale or open schema, for CI.
4. Choose where the backend is served
3.x and 4.x served backend/app.ts at /api by default. In 5.0 createWebApp serves it over HTTP only at an apiPrefix the app sets; without one the backend is loader-only, reached in-process through ctx.api, and a browser call to /api reaches page routing. nifra migrate layout adds apiPrefix = "/api" when it moves a root backend.ts, and nifra check reports a framework config that sets none (NF-C035).
// doc-check: skip - backend/framework.ts
export { reactAdapter as adapter } from "@nifrajs/web-react"
// Serve backend/app.ts at /api, as 3.x and 4.x did by default. "" keeps it loader-only.
export const apiPrefix = "/api"With both nifra.config.ts and backend/framework.ts, every command takes apiPrefix, apiStrip, mounts, csp, nonce, serverComponents and use from backend/framework.ts when nifra.config.ts leaves them unset. The backend sees each request's full path unless apiStrip is on, so its routes are registered under the prefix; nifra routes marks one outside it as in-process only, and nifra dev and nifra build warn about it.
5. Review the 5.0 defaults that changed
- Root status pages (
_404.tsx,_<status>.tsx) and_error.tsxrender inside the layouts above them. One that imports the root layout to wrap itself renders it twice (NF-C034); remove the wrap, or exportinLayouts = false. - A layout's
metareceives its own loader data asdataand the page's aspageData(NF-C040). - A WebSocket handshake runs the route chain an HTTP
GETat the same point runs, so guards added beforeapp.ws()apply to it. - A
Responsereturned to more than one request is no longer shared; build one per request. nifra checkfails a secret comparison (NF-S002) outside browser code of a web app.
nifra upgrade prints the full list with the finding or the one-line change that keeps the 4.x behavior for each.
6. Run the release gates
- Run
nifra check --jsonand fix every error.nifra doctorlists any package still on another release. - If the project has
nifra.assurance.ts, runnifra assure --json. - Run the application test suite and a production build.
- Exercise the pages that load or post data, and every browser call to the backend, under
nifra devand against the production build.