
# 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](/docs/lifecycle), error handling, shutdown), use [plugins](/docs/plugins) instead.
::

| | Modules | [Plugins](/docs/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:

```ts [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:

| 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](/docs/plugins)).

Only two patterns are auto-registered (with any supported extension: `.ts`, `.mts`, `.cts`, `.js`, `.mjs`, `.cjs`, `.tsx`, `.jsx`):

- `modules/*.ts` (a single-file module)
- `modules/*/index.ts` (a directory module)

Any other file inside `modules/` that does not match these patterns (for example a module's own `runtime/` files) is ignored by the scanner.

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](/docs/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`](https://github.com/nitrojs/nitro/blob/main/src/types/nitro.ts) instance. Use the `NitroModule` type from `nitro/types` for type safety:

```ts [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:

| Property | Description |
| --- | --- |
| `nitro.options` | Resolved config (mutable): `handlers`, `virtual`, `plugins`, `runtimeConfig`, and every other option. |
| `nitro.hooks` | Build-time [hooks](#build-time-hooks) (`hookable` instance). |
| `nitro.logger` | Tagged [consola](https://github.com/unjs/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()`:

```ts [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:

| 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. |
| `vite:compile:before` | `(nitro) => void` | Vite builder only. After public assets are copied and prerendering finished, before the build output is finalized (the server bundle is compiled, or skipped for static builds). Use it to emit extra files into the public output directory. |
| `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](https://github.com/nitrojs/nitro/blob/main/src/types/hooks.ts) for the full list and exact signatures, and the [prerendering guide](/docs/prerendering#hooks) for the `prerender:*` hooks in context.

::tip
Runtime hooks (`request`, `response`, `error`) are **not** available here; they belong to [plugins](/docs/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:

```ts [my-nitro-module/src/index.ts]
import type { NitroModule } from "nitro/types";

export default {
  name: "my-nitro-module",
  setup(nitro) {
    // ...
  },
} satisfies NitroModule;
```

```ts [nitro.config.ts]
import { defineConfig } from "nitro";

export default defineConfig({
  modules: ["my-nitro-module"],
});
```

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:

```ts [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:

::code-group

```ts [vite.config.ts]
import { defineConfig } from "vite";
import { nitro } from "nitro/vite";
import myPlugin from "my-plugin";

export default defineConfig({
  plugins: [nitro(), myPlugin()],
});
```

```ts [nitro.config.ts]
import { defineConfig } from "nitro";
import myPlugin from "my-plugin";

export default defineConfig({
  modules: [myPlugin()],
});
```

::

Bundlers ignore the extra `nitro` key, and Nitro ignores the plugin hooks, so neither side needs to know about the other.
