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.

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", () => {
        // ...
      });
    },
  ],
});

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 }.
Wrapper object{ nitro: { name?, setup } }An object with a nitro key. Useful for packages that integrate with multiple tools from a single export.

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.

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
types:extend(types) => voidWhen generating TypeScript types. Extend generated route types and tsConfig.
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.

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

  • Keep modules idempotent. In development, options can be re-processed on config changes — 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 — 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.