Server Compatibility
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. And it's also possible for users to use a custom server runtime based on Web APIs which is compatible everywhere (nuxt/server).
To assist in the upgrade from Nuxt 4, module code written for Nitro v2 can keep running through a temporary compatibility layer. This guide covers what the layer does for you and how to migrate to full support for Nuxt 4 and 5
The Compatibility Layer
A module's server code enters the compatibility layer when its files import from h3, nitropack, nitropack/runtime, #internal/nitro or #imports. (A file that imports nuxt/server or nitro/* is left alone.)
Inside this compatibility layer, h3 resolves to an h3 v1 helper surface built on h3 v2, the nitropack specifiers resolve to their Nitro v3 equivalents, useRuntimeConfig(event) keeps its per-request behavior, and we inject the event.node, event.context.nitro and event.context._nitro.routeRules shapes that v1 code might read.
To help with migration, Nuxt will log which modules it applied the layer to:
WARN [NUXT_B9003] Nitro v2 compatibility was applied to server code from 2 modules, because of what it imports:
- @nuxtjs/robots (imports `h3`, `nitropack/runtime`, `#imports`)
- nuxt-site-config (imports `h3`, `nitropack/runtime`)
nitropack v2 happened to hoist into the server bundle, like lru-cache or similar. Always declare your module's dependencies in package.json.Recommended Migration
nuxt/server 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.
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'),
},
})
},
})
In most cases, your handler files will differ only in their imports:
import { defineEventHandler, useRuntimeConfig } from 'nuxt/server'
export default defineEventHandler(() => ({
version: useRuntimeConfig().public.myModule.version,
}))
// @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,
}))
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 directly, Nitro v3 through the compatibility layer |
nitro3 | nitro, nitro/h3 | 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.
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).
@nuxt/kit utilities, make sure it is in your module's dependencies, which is what the module starter sets up. If you moved @nuxt/kit to peerDependencies, the application's version applies.Server Plugins
Server plugins take variants too, although they are specific to Nitro so there is no generic nuxt variant to register.
defineServerPlugin 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.import { addNitroPlugin, createResolver, defineNuxtModule } from '@nuxt/kit'
export default defineNuxtModule({
meta: { name: 'my-module' },
setup () {
const { resolve } = createResolver(import.meta.url)
addNitroPlugin({
nitro3: resolve('./runtime/server/plugin'),
nitro2: resolve('./runtime/server/plugin.legacy'),
})
},
})
addServerImports, addServerImportsDir and addServerTemplate do not differ between Nitro 2 and 3. If you have code that differs, you can use getNitroVersion to conditionally register imports/templates differently.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:
export default defineNuxtModule({
meta: {
name: 'my-module',
compatibility: { server: 'nuxt' },
},
})
If we can't tell, code will be treated as nitro2.
Nitro-specific Code
nuxt/server covers request and response work: handlers, errors, query and body reading, headers, cookies, redirects, route rules and runtime config. 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 nitro3 file | In a nitro2 file |
|---|---|---|
Storage: useStorage() and its driver configuration | nitro/storage | nitropack/runtime |
Sessions: useSession, getSession, updateSession, clearSession | nitro/h3 | h3 |
Caching: defineCachedHandler(), defineCachedFunction() | nitro/cache | nitropack/runtime |
Nitro's runtime hooks (useNitroApp().hooks, including render:html) and tasks are obviously also not supported by nuxt/server.
Notable Changes in Nitro
These behaviors changed with Nitro v3 and may require further updates in your code:
- Routed middleware no longer runs for subpaths.
nitropackv2 mounted middleware with a route so that it ran for that route and everything below it (such as/path/subpath), with the route stripped fromevent.path. Nitro v3 matches it exactly, like any other handler. When updating, widen therouteto/path/**if you mean to cover both, and read the full path fromevent.path. - A handler registered with no
routewas global middleware. Nitro v3 requires a route. If you need middleware, register it withmiddleware: trueand an explicit route, even if it is intended to run on every path/**. beforeResponseandafterResponseare not Nitro v3 hooks. They will work in the compatibility layer, but you should move to theresponsehook, which receives the builtResponse. Note that (even with the compatibility layer) a hook that replacesresponse.body, or reads it while streaming, is not supported.globalThis.$fetchis not present in Nitro 3. You can useevent.$fetchor import$fetchfrom#imports/server.import.meta.urlis no longer the server entry. Nitro v2 rewrote it to a global pointing at the entry, but Nitro v3 emits the real value. Resolve assets from a path you control instead.
Publish & Share Your Module
Join the Nuxt module ecosystem and publish your module to npm.
Custom Routing
In Nuxt, your routing is defined by the structure of your files inside the pages directory. However, since it uses vue-router under the hood, Nuxt offers you several ways to add custom routes in your project.