---
title: "Server Compatibility"
description: "Ship one module that serves Nuxt 4 and Nuxt 5, whichever server runtime is underneath."
canonical_url: "https://nuxt.com/docs/4.x/guide/modules/server-compatibility"
---
# Server Compatibility

> Ship one module that serves Nuxt 4 and Nuxt 5, whichever server runtime is underneath.

Nuxt supports multiple server runtimes. Nuxt 3 and 4 target Nitro v2 (`nitropack`), which uses `h3` v1; Nuxt 5 defaults to Nitro v3 (`nitro`), which uses `h3` v2. It is also possible to use a custom server runtime based on Web APIs, which is compatible everywhere (`nuxt/server`).

This guide covers how to write a module whose server code runs on all of them.

::note
If your module has no server code, nothing here applies.
::

## Recommended Migration

[`nuxt/server`](https://nuxt.com/docs/4.x/guide/going-further/server-imports) is the import surface for writing agnostic server code based on Web APIs, and it ships in Nuxt 4.6+. Code written with it runs on `nitropack` v2, on Nitro v3, and under other server builders, such as `@nuxt/vite-server`.

### Event Handlers

If you are registering event handlers, keep your current handler (for compatibility with Nuxt <4.6) and add a new portable one using `nuxt/server` beside it, and register both. Nuxt will register the implementation the application can run: the portable file under Nitro v3 and under other builders, the v2 file on a host still running `nitropack` v2.

```ts [module.ts]
import { addServerHandler, createResolver, defineNuxtModule } from '@nuxt/kit'

export default defineNuxtModule({
  meta: { name: 'my-module' },
  setup () {
    const { resolve } = createResolver(import.meta.url)

    addServerHandler({
      route: '/api/my-module/status',
      handler: {
        nuxt: resolve('./runtime/server/status'),
        nitro2: resolve('./runtime/server/status.legacy'),
      },
    })
  },
})
```

The two files differ in their imports, and wherever the handler uses an h3 v1 helper or behavior that `nuxt/server` handles differently:

::code-group
```ts [runtime/server/status.ts]
import { defineEventHandler, useRuntimeConfig } from 'nuxt/server'

export default defineEventHandler(() => ({
  version: useRuntimeConfig().public.myModule.version,
}))
```

```ts [runtime/server/status.legacy.ts]
// @ts-expect-error `#imports` is typed for the app, not the server build
import { useRuntimeConfig } from '#imports'
import { defineEventHandler } from 'h3'

export default defineEventHandler(event => ({
  version: useRuntimeConfig(event).public.myModule.version,
}))
```
::

::read-more{to="https://nuxt.com/docs/4.x/getting-started/upgrade#differences-from-h3-v1"}
See every difference between h3 v1 and `nuxt/server`, and what to use instead.
::

The keys indicate which server API each file relies upon:

| Key      | The file imports                                      | Where it runs                     |
| -------- | ----------------------------------------------------- | --------------------------------- |
| `nuxt`   | `nuxt/server` only                                    | Any server builder, from Nuxt 4.6 |
| `nitro2` | `h3`, `nitropack/runtime`, `#imports`                 | `nitropack` v2                    |
| `nitro3` | `nitro`, `nitro/h3`, alone or alongside `nuxt/server` | The Nitro server builder, v3      |

Nuxt will pick the most appropriate handler to use based on the server builder a user is using. If you register a handler which the user's server builder cannot run, it will be skipped with a warning.

::tip
Use `nitro3` only for code that needs an API only Nitro v3 offers. Using `nuxt/server` will keep your code more portable in future.
::

Using this pattern means you will support both old (<4.6) and new versions of Nuxt (including Nuxt 5).

::important
The object form of `handler` needs `@nuxt/kit@^4.6` in your module's `dependencies`, which is what the [module starter](https://nuxt.com/docs/4.x/guide/modules/getting-started) sets up. If `@nuxt/kit` is only a `devDependency` and left external when you build, or is a `peerDependency`, the application's version runs instead, and a version older than 4.6 fails on the object `handler`.
::

To typecheck the `nuxt` files, install `nuxt@>=4.6` as a `devDependency`. To typecheck `nitro3` files, also install `nitro`.

If your module's own aliases already resolve to the right code on both Nitro majors, register the same file for both keys (`{ nitro2: file, nitro3: file }`) or set [`meta.compatibility.server`](#declaring-what-you-cannot-infer).

On a `nitropack` v2 host, Nuxt prefers the `nitro2` variant, so the `nuxt` variant only runs there if it is the only one registered. Test the `nuxt` variant on Nuxt 5.

In playgrounds and test fixtures on Nuxt 5, h3's auto-imported `defineEventHandler` is not available: import it from `nuxt/server`.

### Dev Server Handlers

`addDevServerHandler` takes the same keys, but each variant is a function in your module code, which runs in the Nuxt process rather than in the server build. `nuxt/server` is for [server code only](https://nuxt.com/docs/4.x/guide/going-further/server-imports#server-code-only), so don't import it from module code, even lazily.

Instead, write the handler against the web `Request` and `Response`, read configuration from `nuxt.options` in `setup`, and adapt it to each server API:

```ts [module.ts]
import { addDevServerHandler, defineNuxtModule } from '@nuxt/kit'
import { defineEventHandler, toWebRequest } from 'h3'

export default defineNuxtModule({
  meta: { name: 'my-module' },
  setup (_options, nuxt) {
    const greeting = nuxt.options.runtimeConfig.public.myModule.greeting

    function greet (request: Request) {
      const name = new URL(request.url).searchParams.get('name')
      if (!name) {
        return new Response('Missing name', { status: 400 })
      }
      return Response.json({ message: `${greeting}, ${name}!` })
    }

    addDevServerHandler({
      route: '/_my-module/greet',
      handler: {
        nuxt: event => greet(event.req),
        nitro2: defineEventHandler(event => greet(toWebRequest(event))),
      },
    })
  },
})
```

The `nuxt` variant receives a `RequestEvent` and reads its `event.req`, and the `nitro2` variant converts the h3 v1 event with `toWebRequest()`. Because the module imports `h3` v1, add `h3@^1` to your module's `dependencies`.

### Server Utilities

If your module exposes utilities that take the event (through an alias, `addServerImports` or a type template), type the parameter as `RequestEvent` from `nuxt/server`, or as the part it reads, such as `Pick<RequestEvent, 'req' | 'context'>`. A parameter typed as `H3Event` only accepts the event of one h3 major, so the utility can't be called from a `nuxt/server` handler.

`getNitroVersion` reports the Nitro major, not which server API the host runs: a host with a different `server.builder` runs none of the Nitro ones.

### Server Plugins

Server plugins take variants too, although they are specific to Nitro so there is no generic `nuxt` variant to register.

::tip
Because `defineNitroPlugin` is an identity function, you may be able to replace it with a type annotation to write code that's portable for both `nitro2` and `nitro3`.
::

```ts [module.ts]
import { addNitroPlugin, createResolver, defineNuxtModule } from '@nuxt/kit'

export default defineNuxtModule({
  meta: { name: 'my-module' },
  setup () {
    const { resolve } = createResolver(import.meta.url)

    addNitroPlugin({
      nitro2: resolve('./runtime/server/plugin.legacy'),
      nitro3: resolve('./runtime/server/plugin'),
    })
  },
})
```

### Other Variants

For anything else that points at server code, such as an alias, a type template or an app plugin that reads the server event, you can use `resolveServerVariant` to resolve the variant that is appropriate for this version of Nuxt, or `undefined` if it runs none of them.

`addServerImports` accepts the same variants in `from`.

```ts [module.ts]
import { addServerImports, createResolver, defineNuxtModule, resolveServerVariant } from '@nuxt/kit'

export default defineNuxtModule({
  meta: { name: 'my-module' },
  setup (_options, nuxt) {
    const { resolve } = createResolver(import.meta.url)

    const someServerFile = resolveServerVariant({
      nuxt: resolve('./runtime/server'),
      nitro2: resolve('./runtime/server.legacy'),
    })

    if (someServerFile) {
      nuxt.options.alias['#my-module/server'] = someServerFile
    }

    addServerImports({
      name: 'verifyToken',
      from: { nuxt: resolve('./runtime/verify'), nitro2: resolve('./runtime/verify.legacy') },
    })
  },
})
```

If running with `nitropack` v2, `nitro2` will always be preferred over `nuxt`, and if running with `nitro` v3, `nitro3` will always be preferred over `nuxt`. So, if you need to test the `nuxt` variant, set `server.builder` in your test fixture to a builder that is not Nitro, such as `@nuxt/vite-server`.

### Declaring What You Cannot Infer

Nuxt reads a registered file's imports to decide which API it uses, so you don't need to declare your compatibility. But if this isn't sufficient, you can set `meta.compatibility.server`:

```ts [module.ts]
export default defineNuxtModule({
  meta: {
    name: 'my-module',
    compatibility: { server: 'nuxt' },
  },
})
```

If we can't tell, code will be treated as `nitro2`.

### Typing Runtime Config and Events

To type runtime config keys your module adds, augment `RuntimeConfig` (or `PublicRuntimeConfig`) from `nuxt/schema`. The keys are then typed in `nuxt`, `nitro2` and `nitro3` files alike.

```ts [module.ts]
declare module 'nuxt/schema' {
  interface RuntimeConfig {
    myModule: { apiKey: string }
  }
}
```

Builder-specific types, such as `H3Event` for `useRequestEvent()` or the full Nitro config in the `nitro:config` hook, come from the generated `.nuxt/nuxt.d.ts`. Without it, they resolve to builder-agnostic types (`RequestEvent` and a reduced Nitro config). Type utilities that accept an event against `RequestEvent` to support any server builder.

### Plugins, Storage and Caching

A module built on server plugins, storage or caching has no `nuxt` variant, because `nuxt/server` doesn't cover those areas. Register its files as `nitro2` now, and add a `nitro3` variant when you port them.

## Nitro-specific Code

`nuxt/server` covers request and response work: handlers, errors, router params, query and body reading and validation, headers, CORS, cookies, sessions, redirects, route rules, runtime config, app config, in-process fetch with `serverFetch()` and runtime hooks with `useServerHooks()`. But Nitro covers a lot more, so if you need any of these areas, you might need to stay on Nitro's own imports inside a `nitro2` or `nitro3` file.

Use the specifiers of the version the file is for. A file that imports `nitro/*` is read as Nitro 3 code, so a `nitro2` file must import `h3` and `nitropack/*` instead:

| Area                                                            | In a `nitro2` file                             | In a `nitro3` file                 |
| --------------------------------------------------------------- | ---------------------------------------------- | ---------------------------------- |
| Storage: `useStorage()` and its driver configuration            | `nitropack/runtime`                            | `nitro/storage`                    |
| Caching: `defineCachedEventHandler()`, `defineCachedFunction()` | `nitropack/runtime`                            | `nitro/cache`                      |
| Server plugins                                                  | `defineNitroPlugin()` from `nitropack/runtime` | `definePlugin()` from `nitro`      |
| Nitro's own runtime hooks, such as `request` and `error`        | `useNitroApp().hooks` from `nitropack/runtime` | `useNitroHooks()` from `nitro/app` |
| Lazy handlers: `lazyEventHandler()`                             | `h3`                                           | `nitro/h3`                         |

Tasks are also not supported by `nuxt/server`.

::read-more{to="https://nuxt.com/docs/4.x/guide/going-further/server-imports#reaching-past-the-surface"}
See which h3 helpers work with the portable event, and which need Nitro-specific utilities.
::

## Notable Changes in Nitro v3

If a `nitro2` file is all you ship, it will keep running on Nuxt 5 through the Nitro v2 compatibility layer, which is documented in the [Nuxt 5 module server compatibility guide](https://nuxt.com/docs/5.x/guide/modules/server-compatibility) along with the behaviours that changed with Nitro v3.


## Sitemap

See the full [sitemap](https://nuxt.com/sitemap.md) for all pages.
