Rsbuild Integration
Use Nitro as an Rsbuild plugin to add a server to your Rsbuild app (experimental).
Warning
The Rsbuild plugin is experimental. Its behavior may change in minor releases.
Nitro integrates with Rsbuild (v2.3+) as a plugin. It adds a full server to your Rsbuild app (API routes, server-side rendering, WebSockets) and builds client and server into a single .output/ directory that deploys with any deployment preset.
#Nitro as an Rsbuild plugin
Add the plugin to your Rsbuild config:
import { defineConfig } from "@rsbuild/core";
import { nitro } from "nitro/rsbuild";
export default defineConfig({
plugins: [nitro({ serverDir: "./" })],
});With the plugin enabled, you use the Rsbuild CLI:
rsbuild dev: One dev server for both frontend and backend. Nitro routes are handled before the Rsbuild middlewares, so they take precedence over client assets. Other requests are served by Rsbuild first (compiled assets, the public directory and HTML pages), then by Nitro. Server code reloads when server files change, while client code keeps Rsbuild's hot module replacement.rsbuild build: Builds the client into the Nitro public output, then the server with the configured preset. Hashed client assets (static/by default) are served with immutable cache headers (unless a/static/**route rule sets its owncache-control), without fallthrough: a request for a missing file there gets a404withCache-Control: no-storeinstead of reaching your routes or renderer.rsbuild build --watch: Builds again when files change. Public assets are copied and routes prerendered again before each server build, and added or removed server routes rebuild the server. The output is only cleaned before the first build. With thestaticpreset, only changes of the client trigger a rebuild.rsbuild preview: Serves the production build locally through Nitro, including static assets and WebSocket support.
Nitro options are passed to nitro() (or set in nitro.config.ts). The server is bundled with the rspack builder config, using the Rspack instance of Rsbuild.
In development, asset loads that Rsbuild cannot serve (a script, style or image request) respond with a 404, even when a catch-all route or the SSR renderer would answer them with a page.
The baseURL of Nitro is the base of the Rsbuild server (server.base): client pages and assets are served under it. When only server.base is set, it is used as the baseURL of Nitro.
#Environments
The plugin uses Rsbuild environments and adds a nitro environment, which builds the server. Every environment with output.target: "web" (the default web environment, unless you define environments) is a client environment, emitted to the public output. With several client environments, pages (the HTML template and the client assets of server-side rendering) are owned by the client environment, then web, then the first one defined.
Every other environment with output.target: "node" is a service: Nitro bundles its output with the server, and you fetch it with fetchService() from nitro/services. Services of the services option are built in a server environment of the same name:
export default defineConfig({
plugins: [nitro({ services: { api: { entry: "./src/api-entry.ts" } } })],
});The entry of a service exports a fetch method (as default.fetch or fetch). Services share the Nitro runtime of the server, so imports of nitro/* (such as nitro/runtime-config or nitro/storage) work in their code.
#Server-side rendering
Nitro detects a file named entry-server.(ts|js|tsx|jsx|mts|mjs) (in the project root, app/, src/, or the server directory) and adds an ssr environment for it, if you have not defined one.
- HTML template: when the Rsbuild
html.templatecontains an<!--ssr-outlet-->comment, Nitro renders pages from the HTML generated by Rsbuild (with the client scripts injected) and replaces the comment with the response of thessrentry. - Full response: otherwise, the
ssrentry renders pages itself, and itsfetchmethod returns the fullResponse.
import { defineConfig } from "@rsbuild/core";
import { nitro } from "nitro/rsbuild";
export default defineConfig({
source: { entry: { index: "./app/entry-client.ts" } },
html: { template: "./index.html" },
plugins: [nitro({ serverDir: "./" })],
});In development, page requests (navigations to paths without a file extension) are rendered by Nitro instead of the client HTML of Rsbuild.
The HTML template is compiled into the server bundle (it can also use renderer template syntax, with fetchService in its context), so no template is compiled at runtime, including on runtimes that disallow it such as Cloudflare Workers. In development, the server is rebuilt when the generated HTML changes.
#Client assets
An SSR entry rendering the whole document (without an HTML template) gets the script and style URLs of a client module with an ?assets import:
import clientAssets from "./entry-client.ts?assets=client";
export default {
async fetch(req: Request) {
const head = [
...clientAssets.css.map(({ href }) => `<link rel="stylesheet" href="${href}">`),
...clientAssets.js.map(({ href }) => `<script defer src="${href}"></script>`),
].join("");
const html = `<!doctype html><html><head>${head}</head><body>...</body></html>`;
return new Response(html, { headers: { "content-type": "text/html" } });
},
};The default export is { entry, js, css, merge() }, as with Vite:
jslists the scripts of the module chunk and the chunks it needs, in load order. Rsbuild emits classic scripts by default, so add each of them as a script (withdefer);entryis the last one (the chunk of the module).csslists the stylesheets of these chunks.merge(...others)combines several results (e.g.clientAssets.merge(pageAssets)), without duplicates.
The module must be an entry or a dynamic import (import()) of a client environment. ?assets=client (or ?assets) targets the client environment owning pages (see Environments), and ?assets=<name> another client environment. ?assets imports work in server routes and in the code of services.
Types of ?assets imports are included with nitro/rsbuild (or add /// <reference types="nitro/rsbuild/types" />).
Note
In development, Rsbuild compiles dynamic imports when the browser loads them (dev.lazyCompilation), so the assets of a dynamic import are empty until then. CSS imported by server code is not emitted by Rsbuild (?assets=ssr is empty).
See the Rsbuild SSR HTML example (HTML template) and the Rsbuild SSR with React example (full document, with ?assets imports).
#Frameworks
Environments added by other Rsbuild plugins are configured too: a framework plugin adding client and ssr environments gets its client output emitted to the Nitro public output, and its ssr environment used as the renderer.
With TanStack Start, add nitro() next to the tanstackStart() plugin of @tanstack/react-start/plugin/rsbuild, and disable its development middleware so that Nitro handles all requests to the ssr environment (pages, server functions and server routes):
import { defineConfig } from "@rsbuild/core";
import { pluginReact } from "@rsbuild/plugin-react";
import { tanstackStart } from "@tanstack/react-start/plugin/rsbuild";
import { nitro } from "nitro/rsbuild";
export default defineConfig({
plugins: [
pluginReact(),
tanstackStart({ rsbuild: { installDevServerMiddleware: false } }),
nitro(),
],
});See the Rsbuild SSR with TanStack Start example.
#Good to know
- Presets running the development server in another runtime (such as
cloudflare-modulewith workerd) are supported, including server environments. - The server is built with the Rspack config of Nitro: Rsbuild rules and transforms of other plugins apply to the client and service environments, not to the
nitroenvironment. UsebuildPluginsfor server build plugins. - With an HTML template, only the
index.htmlpage of the client environment owning pages is used.