Creating a Builder
A builder is the part of Nuxt responsible for bundling your application. Nuxt ships with three official builders, Vite (the default), webpack and Rspack, and you can select one with the builder option or supply your own.
This guide explains how builders fit into the Nuxt build, what contract a builder must fulfil, and how to author one.
What a Builder Does
Nuxt separates what to build from how to build it.
The Nuxt core (the nuxt context described in How Nuxt Works) and your modules generate the virtual application: the entrypoints, route table, plugins, component registry, and the rest of the virtual file system under #build. The builder takes that virtual application and turns it into real JavaScript and CSS bundles, and in development runs a dev server that serves and hot-reloads them.
Concretely, a builder is responsible for:
- Bundling the client build (the browser bundle) and, when SSR is enabled, the server build (the SSR app entry).
- Producing the artifacts the server runtime needs to render and hydrate: the client manifest, the per-component styles map, and so on (see The Build Output Contract).
- In development, exposing a dev server and triggering reloads when the build changes.
The deployable server itself is produced by Nitro via @nuxt/nitro-server, not by the builder. The builder hands its outputs to Nitro through a typed contract; Nitro bundles them into the final .output.
The Builder Interface
A builder is an object implementing the NuxtBuilder interface. The only required method is bundle:
import type { Nuxt } from '@nuxt/schema'
export interface NuxtBuilder {
bundle: (nuxt: Nuxt) => Promise<void>
/**
* Optional. When the user opts in via `experimental.watcher: 'builder'`,
* Nuxt calls this instead of starting its own dev file watcher, letting
* the builder reuse its own watcher. The builder should register a
* `nuxt.hook('close', ...)` to clean up.
*/
setupWatcher?: (nuxt: Nuxt) => Promise<void> | void
}
Nuxt resolves the active builder from the builder option. It accepts either a module specifier that default-exports a NuxtBuilder, or an inline object:
export default defineNuxtConfig({
// a package that exports `{ bundle }`
builder: '@nuxt/vite-builder',
})
import type { NuxtBuilder } from '@nuxt/schema'
const myBuilder: NuxtBuilder = {
async bundle (nuxt) {
// ...
},
}
export default defineNuxtConfig({
builder: myBuilder,
})
Nuxt calls bundle(nuxt) once during nuxt build and nuxt dev, after the virtual application has been generated and the build:before hook has fired. Nuxt wraps your bundle so that any thrown error triggers the build:error hook automatically.
@nuxt/vite-builder, @nuxt/webpack-builder, @nuxt/rspack-builder) are recognised by name for builder-specific behaviour elsewhere in Nuxt. A custom builder still works through the generic contract described below.The Build Lifecycle
When you run nuxt build or nuxt dev, Nuxt:
- Creates the
nuxtcontext and runs modules, populatingnuxt.optionsand the build hooks. - Generates the virtual application (templates, route table, plugins) into the
#buildvirtual file system. - Fires
build:before. - Resolves the builder and calls
builder.bundle(nuxt). This is where your builder runs. - Fires
build:done, and in production closes thenuxtinstance.
Your bundle implementation typically branches on nuxt.options.dev:
- In production, it runs the client and (if
nuxt.options.ssr) server builds to completion, writing artifacts tonuxt.options.buildDirand registering them as build outputs. - In development, it sets up a dev server, starts a watching build, assigns
nuxt.server, and keeps running.
A builder communicates with the rest of Nuxt almost entirely through hooks. It reads build configuration off nuxt.options and, when appropriate, lets modules extend its bundler configuration.
Letting Modules Extend the Bundler
Modules influence the bundle through Nuxt Kit helpers. A builder should honour the relevant ones:
addVitePlugin/addWebpackPluginregister bundler-specific plugins.addBuildPluginregisters an unplugin factory, so a single plugin works across every builder.extendViteConfig/extendWebpackConfigmutate the resolved bundler config.
The official builders also emit their own hooks (for example vite:extendConfig, vite:serverCreated, webpack:config) so modules and Nitro can participate in the build. A custom builder may emit its own hooks, but the build output contract below is what makes it interoperate with the Nuxt server runtime.
The Build Output Contract
The server runtime (@nuxt/nitro-server) does not know which builder produced the app. It imports each build artifact through a stable nuxt/* subpath, and the active builder populates those subpaths with nuxt.buildOutputs. This is the build output contract.
The contract is declared by the NuxtBuildOutputs interface in @nuxt/schema:
export interface NuxtBuildOutputs {
/** Module body re-exporting the SSR app entry. */
serverEntry: () => string | Promise<string>
/** Path to the emitted per-component SSR styles map, or `undefined` when no inline styles are produced. */
ssrStyles: string | undefined
/** Serialized client manifest for `vue-bundle-renderer`. */
clientManifest: () => string | Promise<string>
/** Serialized precomputed client dependency data for `vue-bundle-renderer`. */
clientPrecomputed: () => string | Promise<string>
/** Module body exporting the hashed entry chunk filename for import maps. */
entryChunkName: () => string | Promise<string>
/** Module body exporting the entry module IDs used for inline style extraction. */
entryIds: () => string | Promise<string>
}
Each key maps to a nuxt/* subpath that the server runtime imports:
| Build output | Subpath | Imported as |
|---|---|---|
serverEntry | nuxt/entry | the SSR app factory passed to createRenderer |
clientManifest | nuxt/manifest | the vue-bundle-renderer client manifest |
clientPrecomputed | nuxt/precomputed | precomputed dependency data |
ssrStyles | nuxt/styles | the per-component inline-styles map |
entryChunkName | nuxt/entry-chunk | the hashed entry chunk filename (import map) |
entryIds | nuxt/entry-ids | the entry module IDs for style extraction |
Each nuxt/* subpath ships a default stub in the nuxt package, so the server runtime always type-checks and builds even before a builder runs. A builder overrides the stub by setting the matching build output; the value it provides replaces the stub at build time.
Two Kinds of Build Output
The keys come in two shapes:
- Value providers (
serverEntry,clientManifest,clientPrecomputed,entryChunkName,entryIds) are functions returning the module body as a string. The string is inlined verbatim into the server bundle, so it must not depend on the file's location on disk.serverEntry, for example, returns a body that re-exports the built SSR entry by absolute specifier:setBuildOutput('serverEntry', () => `export { default } from ${JSON.stringify(serverEntryURL)}`) - Emitted-file path (
ssrStyles) is an absolute path (not code) to a real module the builder emitted. The runtime'snuxt/stylesimport resolves to that file so the deployable's bundler resolves the styles map's relative sibling imports against the file's own location. Modelling it as a code string would strip that directory context and break the relative imports, so this one is a path:setBuildOutput('ssrStyles', resolve(serverOutDir, 'styles.mjs'))
LeavessrStylesasundefined(its default) when the build produces no inline styles; the runtime falls back to an empty styles map.
Setting Build Outputs
Use the setBuildOutput helper from @nuxt/kit:
import { setBuildOutput } from '@nuxt/kit'
setBuildOutput('clientManifest', () => 'export default ' + serializedManifest)
setBuildOutput writes to nuxt.buildOutputs[key]. From inside a bundler plugin that already holds the nuxt instance you can assign nuxt.buildOutputs[key] directly; setBuildOutput is the convenience for code that resolves nuxt via useNuxt().
A provider can be asynchronous and is read lazily, when the server build resolves the corresponding nuxt/* import. This lets a builder register the provider early (for example before the client build has finished) and have it return the finalised value once the data exists.
A Minimal Example
The skeleton of a builder that fulfils the contract for a production build:
import { resolve } from 'node:path'
import { pathToFileURL } from 'node:url'
import { setBuildOutput } from '@nuxt/kit'
import type { NuxtBuilder } from '@nuxt/schema'
export const bundle: NuxtBuilder['bundle'] = async (nuxt) => {
const serverDir = resolve(nuxt.options.buildDir, 'dist/server')
// ...run your client and server bundles here, writing artifacts to disk...
const { serializedClientManifest } = await runBundles(nuxt, serverDir)
if (nuxt.options.ssr) {
// Point `nuxt/entry` at the built SSR app entry.
const serverEntryURL = pathToFileURL(resolve(serverDir, 'server.mjs')).href
setBuildOutput('serverEntry', () => `export { default } from ${JSON.stringify(serverEntryURL)}`)
// Provide the client manifest produced by your client build.
setBuildOutput('clientManifest', () => `export default ${serializedClientManifest}`)
// If you emit a per-component styles map alongside its CSS chunks:
setBuildOutput('ssrStyles', resolve(serverDir, 'styles.mjs'))
}
}
serverEntry default is a no-op app and most other outputs are unused.The Development Server
In development, a builder is also responsible for serving and hot-reloading the app. Two things matter:
nuxt.serverholds the running dev server. Nuxt's CLI consumes it, and the official builders expose ahandler(a Node request listener), afetch(a webfetchhandler), andreload/closemethods on it. Whether you build your own dev server or delegate to Nitro's (createDevServerfromnitro/builder) is up to the builder.- Reloads are signalled by calling
nuxt.server.reload()when a compilation finishes. The official builders emit acompiledhook (for examplevite:compiled,webpack:compiled) that the server integration listens to in order to reload.
In dev, the build outputs are typically wired to live, in-memory sources rather than on-disk files. For instance, the SSR entry may be served from the bundler's in-memory output, and the client manifest may be computed from the dev module graph, rather than read from nuxt.options.buildDir.