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'),
},
})
},
})
The two files differ in their imports, and wherever the handler uses an h3 v1 helper or behavior that nuxt/server handles differently:
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, 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.
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).
handler needs @nuxt/kit@^4.6 in your module's dependencies, which is what the module starter 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.
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.
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.
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'),
})
},
})
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.
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 the imports of each registered file, and of the files it imports, to decide which API it uses, so you don't need to declare your compatibility. Type-only imports (import type) are ignored, as are files in your runtime directory that no registered file imports. 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.
Nuxt reads the imports of your module's own files, not of the packages they call. A nuxt file that calls a library which imports Nitro internally is accepted, but fails under a builder other than Nitro.
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.
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 nitro3 file | In a nitro2 file |
|---|---|---|
Storage: useStorage() and its driver configuration | nitro/storage | nitropack/runtime |
Caching: defineCachedHandler(), defineCachedFunction() | nitro/cache | nitropack/runtime |
| Server plugins | definePlugin() from nitro | defineNitroPlugin() from nitropack/runtime |
Nitro's own runtime hooks, such as request, response and error | useNitroHooks() from nitro/app | useNitroApp().hooks from nitropack/runtime |
Lazy handlers: lazyEventHandler() | nitro/h3 | h3 |
Tasks: defineTask(), runTask() | nitro/task | nitropack/runtime |
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. For a route of the app, useserverFetch(event, path)fromnuxt/server. You can also 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.