A host is a Request in and a Response out. You do not normally write the
server that calls it: Nitro builds one around your route
tree, and where it runs is a preset in vite.config.ts.
Building
Three steps, and only the first is required:
npm run dev # vite — serves from source, no build step
npm run build # bundles, renders every route once, then Nitro assembles .output/
npm run start # runs .output/server/index.mjsdev is Vite’s own dev server, and edits reach the browser without a reload:
| you edit | what happens |
|---|---|
| a client component | Fast Refresh — the code updates and its state survives |
| a page or layout | the payload is re-fetched and the tree re-rendered |
| adding or deleting a page | the server restarts to pick up the new route table |
The browser never holds a server component, so Vite cannot hot-swap it. Instead the client re-fetches the page — which remounts any client component below, so their state resets.
Edit a client component directly and its state does survive; that is Fast Refresh.
The restart on a new page is because the route tree is read when the server starts: a page that appears later would otherwise 404 while sitting right there on disk.
build ends by rendering every route once and storing what it can, which is
what turns a route into a file on disk instead of a render per request.
It is part of build rather than a separate command because forgetting it costs
you everything and looks like nothing — every page still works, each one just
renders again for every visitor.
Turn it off with rscKit({ prerender: false }) when the build machine
cannot do what the pages need. See Static
generation.
Compiling to a single binary
With the Bun preset the whole application ends up in one file:
npm run compile # builds, then bun build --compile
./dist/appThat works because the generated config sets serveStatic: 'inline'. Without
it the binary compiles, starts, serves pages, and 404s every asset — the static
path resolves into Bun’s virtual filesystem, where the files on disk are not.
What it costs
Measured on the example — eleven routes, 240 KB of assets, 164 KB of stored pages:
| compiled | run from .output/ |
|
|---|---|---|
| Cold start | 45 ms | 56–65 ms |
| Resident, idle | 18 MB | 19 MB |
| Resident, after 300 requests | 27 MB | 32 MB |
| On disk | 62 MB | 240 KB + a runtime |
Memory is flat between the two: the binary holds its assets as Response
objects built at boot, which for 240 KB is nothing, and both settle in the high
twenties once React has warmed up. A live render is what moves it — 40 MB after
200 of them — and that is the renderer, not the packaging.
If you work with an AI agent
The scaffold writes an AGENTS.md beside your README.md. Claude Code, Cursor
and the rest read it, and it covers the things an agent otherwise gets wrong
from React or Next habits — "use client" versus "use server", where an
authorisation check belongs, which props are async, and that the build output is
worth reading rather than ignoring.
Beside it is .mcp.json, which connects the MCP server: Claude
Code asks you to approve it on first use, and from then on an agent can read
what your last build actually did instead of guessing.
And there is a test to extend, tests/app.test.ts, which goes through the
real build: bun run check runs typecheck, lint and tests together, and is
the one command an agent — or you — runs before calling something done.
Edit it as your project grows. It is yours; nothing regenerates it.
There is also an MCP server, which answers from your actual build rather than from memory:
claude mcp add rsc-kit -- npx -y @rsc-kit/mcpIt can say why a particular page is not static, what each route ships to the browser, and how to build a form or an api route the way this framework expects. See Working with an AI agent.