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 run at build time. To extend the server's runtime behavior (request lifecycle, error handling, shutdown), use plugins instead.

ModulesPlugins
RunOnce, during initialization and buildOnce, on server startup
Receivenitro (build context)nitroApp (runtime app)
Typical useModify config, add handlers and virtual files, hook into buildHook into request/response lifecycle
Bundled into outputNoYes

#Using modules

Register modules with the modules config array:

nitro.config.ts
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:

FormExampleDescription
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:

modules/hello.ts
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:

PropertyDescription
nitro.optionsResolved config (mutable): handlers, virtual, plugins, runtimeConfig, and every other option.
nitro.hooksBuild-time hooks (hookable instance).
nitro.loggerTagged consola logger for build-time output.
nitro.metaNitro version info (version, majorVersion).

#Build-time hooks

Modules can hook into the build lifecycle with nitro.hooks.hook():

modules/build-info.ts
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:

HookSignatureWhen it runs
build:before(nitro) => voidBefore the build starts, before the bundler config is created.
rollup:before(nitro, config) => voidAfter the bundler (Rollup / Rolldown) config is resolved, right before bundling. Last chance to modify it.
compiled(nitro) => voidAfter the server bundle is written to the output directory.
dev:start() => voidIn development, when the dev worker starts.
dev:reload(payload?) => voidIn development, after each rebuild when the dev worker reloads.
dev:error(cause?) => voidIn development, when a build error occurs.
prerender:routes(routes: Set<string>) => voidBefore prerendering. Add or remove routes to prerender.
prerender:config(config) => voidWhen the prerenderer's Nitro config is created.
prerender:generate(route, nitro) => voidFor each route, before it is generated.
prerender:route(route) => voidFor each route, after it is generated.
prerender:done({ prerenderedRoutes, failedRoutes }) => voidAfter prerendering finishes.
close() => voidWhen 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 into plugins, users of a standalone Nitro app into modules, 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.options and extend them instead of replacing them. Let users opt out of your module's behavior.
  • Use nitro.logger for build-time output instead of console.
  • 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:

my-plugin/src/index.ts
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.

Nitro  builds full-stack servers that deploy anywhere.