
# 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](https://rsbuild.rs) (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](/deploy).

## Nitro as an Rsbuild plugin

Add the plugin to your Rsbuild config:

```ts [rsbuild.config.ts]
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 own `cache-control`), without fallthrough: a request for a missing file there gets a `404` with `Cache-Control: no-store` instead 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 the `static` preset, 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`](/config/build#builder) 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`](/config/routing#baseurl) of Nitro is the base of the Rsbuild server ([`server.base`](https://rsbuild.rs/config/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](https://rsbuild.rs/guide/advanced/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`](/config/build#services) option are built in a server environment of the same name:

```ts [rsbuild.config.ts]
export default defineConfig({
  plugins: [nitro({ services: { api: { entry: "./src/api-entry.ts" } } })],
});
```

```ts [routes/api/[...].ts]
import { defineHandler } from "nitro";
import { fetchService } from "nitro/services";

export default defineHandler((event) => fetchService("api", event.req));
```

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.template`](https://rsbuild.rs/config/html/template) contains 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 the `ssr` entry.
- **Full response**: otherwise, the `ssr` entry renders pages itself, and its `fetch` method returns the full `Response`.

```ts [rsbuild.config.ts]
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: "./" })],
});
```

```ts [app/entry-server.ts]
export default {
  async fetch(req: Request) {
    return `<h1>Rendered for ${new URL(req.url).pathname}</h1>`;
  },
};
```

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](/docs/renderer) 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:

```ts [app/entry-server.ts]
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](/docs/vite):

- `js` lists 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 (with `defer`); `entry` is the last one (the chunk of the module).
- `css` lists 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](#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`](https://rsbuild.rs/config/dev/lazy-compilation)), 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](/examples/rsbuild-ssr-html) (HTML template) and the [Rsbuild SSR with React example](/examples/rsbuild-ssr-react) (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](https://tanstack.com/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):

```ts [rsbuild.config.ts]
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](/examples/rsbuild-ssr-tss-react).

## Good to know

- Presets running the development server in another runtime (such as `cloudflare-module` with 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 `nitro` environment. Use [`buildPlugins`](/config/build#buildplugins) for server build plugins.
- With an HTML template, only the `index.html` page of the client environment owning pages is used.
