Modules
Use modules to extend Nitro's build-time behavior.
Nitro modules are functions that run once when the Nitro instance is initialized (in development, build, and prerendering). They receive the nitro build context, which they can use to modify options, register build-time hooks, add virtual files, or register route handlers.
Note
| Modules | Plugins | |
|---|---|---|
| Run | Once, during initialization and build | Once, on server startup |
| Receive | nitro (build context) | nitroApp (runtime app) |
| Typical use | Modify config, add handlers and virtual files, hook into build | Hook into request/response lifecycle |
| Bundled into output | No | Yes |
#Using modules
Register modules with the modules config array:
import { defineConfig } from "nitro";
export default defineConfig({
modules: [
// Path to a local module (relative to the project root)
"./modules/my-module.ts",
// Name of an npm package
"my-nitro-module",
// Inline module object
{
name: "inline-module",
setup(nitro) {
nitro.logger.info("Inline module setup");
},
},
// Bare setup function
(nitro) => {
nitro.hooks.hook("compiled", () => {
// ...
});
},
// Plugin object with a `nitro` key
{
name: "plugin-shaped-module",
nitro: {
setup(nitro) {
nitro.logger.info("Plugin module setup");
},
},
},
],
});Each entry can be one of the following forms:
| Form | Example | Description |
|---|---|---|
| Path or package string | "./modules/my-module.ts", "my-nitro-module" | Resolved from the project root and imported. The default export is used as the module. |
| Module object | { name?, setup } | An object with a setup(nitro) function and an optional name. |
| Bare function | (nitro) => { ... } | A shorthand for { setup }. |
| Plugin object | { name?, nitro: { setup } } | An object with a nitro key holding the module. This matches the Rollup/Vite plugin shape, so one export can be both a bundler plugin and a Nitro module. |
Files in the modules/ directory are automatically registered as modules (like plugins/ for runtime plugins).
Modules referenced by path or package name are resolved and installed only once; duplicate entries pointing to the same file are skipped.
Tip
In a Vite project, any plugin in vite.config.ts that carries a nitro key is registered as a Nitro module automatically. There is no need to also list it in modules.
#Authoring a module
A module is an object with a setup function receiving the Nitro instance. Use the NitroModule type from nitro/types for type safety:
import type { NitroModule } from "nitro/types";
export default {
name: "hello",
setup(nitro) {
// Add a virtual module usable anywhere in the server code as `#hello`
nitro.options.virtual["#hello"] = `export const hello = "world";`;
// Register a route handler pointing to the virtual module
nitro.options.handlers.push({
route: "/_hello",
handler: "#hello-handler",
});
nitro.options.virtual["#hello-handler"] = /* ts */ `
import { defineHandler } from "nitro";
import { hello } from "#hello";
export default defineHandler(() => ({ hello }));
`;
},
} satisfies NitroModule;The setup function can be async. Useful properties on the nitro instance:
| Property | Description |
|---|---|
nitro.options | Resolved config (mutable): handlers, virtual, plugins, runtimeConfig, and every other option. |
nitro.hooks | Build-time hooks (hookable instance). |
nitro.logger | Tagged consola logger for build-time output. |
nitro.meta | Nitro version info (version, majorVersion). |
#Build-time hooks
Modules can hook into the build lifecycle with nitro.hooks.hook():
import type { NitroModule } from "nitro/types";
export default {
name: "build-info",
setup(nitro) {
nitro.hooks.hook("compiled", () => {
nitro.logger.info(`Server built in ${nitro.options.output.dir}`);
});
},
} satisfies NitroModule;The most useful hooks:
| Hook | Signature | When it runs |
|---|---|---|
build:before | (nitro) => void | Before the build starts, before the bundler config is created. |
rollup:before | (nitro, config) => void | After the bundler (Rollup / Rolldown) config is resolved, right before bundling. Last chance to modify it. |
compiled | (nitro) => void | After the server bundle is written to the output directory. |
dev:start | () => void | In development, when the dev worker starts. |
dev:reload | (payload?) => void | In development, after each rebuild when the dev worker reloads. |
dev:error | (cause?) => void | In development, when a build error occurs. |
prerender:routes | (routes: Set<string>) => void | Before prerendering. Add or remove routes to prerender. |
prerender:config | (config) => void | When the prerenderer's Nitro config is created. |
prerender:generate | (route, nitro) => void | For each route, before it is generated. |
prerender:route | (route) => void | For each route, after it is generated. |
prerender:done | ({ prerenderedRoutes, failedRoutes }) => void | After prerendering finishes. |
close | () => void | When the Nitro instance is closed. |
All hooks can be async. See the hooks source for the full list and exact signatures, and the prerendering guide for the prerender:* hooks in context.
Tip
Runtime hooks (request, response, error) are not available here; they belong to plugins. A module can still add runtime behavior by pushing a plugin file to nitro.options.plugins.
#Best practices
- Give every module a
name. It makes the module recognizable in config and, on a plugin object, is what Rollup and Vite require to identify the plugin. - Prefer the plugin object form.
{ name, nitro: { setup } }is understood by Nitro and by Rollup and Vite, so a single export can serve both. Users of a Vite app drop it intoplugins, users of a standalone Nitro app intomodules, and the module runs either way. - Keep modules idempotent. In development, options can be re-processed on config changes, so check before pushing duplicate handlers, plugins, or virtual entries.
- Prefer hooks over patching. Use build hooks to react to lifecycle events instead of monkey-patching Nitro internals or overwriting functions.
- Respect user options. Read existing values from
nitro.optionsand extend them instead of replacing them. Let users opt out of your module's behavior. - Use
nitro.loggerfor build-time output instead ofconsole. - Mind the runtime bundle. Any handler or plugin your module adds is bundled for every deployment target, so keep runtime code minimal and runtime-agnostic.
#Distributing modules
To share a module as an npm package, export the module object as the default export and users can reference it by package name:
import type { NitroModule } from "nitro/types";
export default {
name: "my-nitro-module",
setup(nitro) {
// ...
},
} satisfies NitroModule;Add nitro as a peer dependency, and import types only from nitro/types so your package does not bundle Nitro itself.
#Modules that are also bundler plugins
If your package already ships a Rollup or Vite plugin, put the Nitro module under a nitro key on the plugin object instead of publishing two entry points:
import type { Plugin } from "vite";
import type { NitroModule } from "nitro/types";
export default function myPlugin(): Plugin & { nitro: NitroModule } {
return {
name: "my-plugin",
transform(code, id) {
// ...
},
nitro: {
setup(nitro) {
nitro.options.virtual["#my-plugin"] = `export const value = "hello";`;
},
},
};
}The same export then works from both config files:
import { defineConfig } from "vite";
import { nitro } from "nitro/vite";
import myPlugin from "my-plugin";
export default defineConfig({
plugins: [nitro(), myPlugin()],
});Bundlers ignore the extra nitro key, and Nitro ignores the plugin hooks, so neither side needs to know about the other.