Migration Guide
Nitro v3 introduces intentional backward-incompatible changes. This guide helps you migrate from Nitro v2.
Note
This is a living document for migrating from Nitro 2 to 3. Please check it regularly while using the beta version.
Most migrations follow the same order:
nitropack dependency to nitro and upgrade to Node.js 20+.#nitropack Is Renamed to nitro
The NPM package nitropack (v2) has been renamed to nitro (v3).
Migration: Update the nitropack dependency to nitro in package.json:
{
"dependencies": {
-- "nitropack": "latest"
++ "nitro": "latest"
}
}See the nightly channel guide for more about nightly builds.
Migration: Search your codebase and rename all instances of nitropack to nitro:
-- import { defineNitroConfig } from "nitropack/config"
++ import { defineConfig } from "nitro"#Minimum Supported Node.js Version: 20
Nitro now requires a minimum Node.js version of 20, as Node.js 18 reached end-of-life in April 2025.
Please upgrade to the latest LTS version (>= 20).
Migration:
- Check your local Node.js version using
node --versionand update if necessary. - If you use a CI/CD system for deployment, ensure that your pipeline is running Node.js 20 or higher.
- If your hosting provider manages the Node.js runtime, make sure it's set to version 20, 22, or later.
#Builder Is Installed On Demand
Nitro bundles your server with rolldown by default (or rollup or vite), but none of them is a dependency of Nitro. The builder is resolved from your project and installed on demand when building. Add it to your project dependencies to pin its version:
npm i -D rolldownWithout rolldown installed, nitro dev runs the server sources without a builder (builder: false).
#Auto-Imports Are Removed
Nitro v2 automatically made utilities such as defineEventHandler, useStorage, useRuntimeConfig, defineCachedFunction, and defineNitroPlugin available in server code without imports (powered by unimport), and also exposed the exports of server/utils/ and a virtual #imports module.
Auto-imports have been removed in Nitro v3. The imports config option, the #imports module, and the generated nitro-imports.d.ts declarations are no longer available. Using these globals will result in build or runtime errors.
Migration: Add explicit imports everywhere. Common utilities and where they come from:
import { defineHandler, definePlugin, defineErrorHandler, HTTPError } from "nitro";
import { useKV } from "nitro/kv";
import { useRuntimeConfig } from "nitro/runtime-config";
import { defineCachedFunction, defineCachedHandler } from "nitro/cache";
import { useDatabase } from "nitro/database";
import { defineTask, runTask } from "nitro/task";
import { useNitroApp, useNitroHooks } from "nitro/app";
import { getQuery, getCookie } from "nitro/h3";Utilities from your own utils/ directory need explicit (relative) imports too:
import { defineHandler } from "nitro";
import { makeGreeting } from "../utils/hello.ts";
export default defineHandler(() => makeGreeting("Nitro"));#Type Imports
Nitro types are now only exported from nitro/types.
Migration: Import types from nitro/types instead of nitro:
-- import { NitroRuntimeConfig } from "nitropack"
++ import { NitroRuntimeConfig } from "nitro/types"#Changed Nitro Subpath Imports
Nitro v2 introduced multiple subpath exports, some of which have been removed or updated:
nitropack/rollup,nitropack/core(usenitro/builder)nitropack/runtime/*(usenitro/*)nitropack/kit(removed)nitropack/presets(removed)nitropack/meta(usenitro/meta, which only exportsversion,runtimeDirandruntimeDependencies)
An experimental nitropack/kit was introduced but has now been removed. A standalone Nitro Kit package may be introduced in the future with clearer objectives.
Migration:
- Use
NitroModulefromnitro/typesinstead ofdefineNitroModulefrom the kit. - Prefer built-in Nitro presets (external presets are only for evaluation purposes).
#Server Directory Scanning Is Opt-In
In Nitro v2, srcDir defaulted to the project root, and directories such as routes/, api/, middleware/, plugins/, and tasks/ were always scanned.
In Nitro v3:
- The
srcDiroption is deprecated in favor ofserverDir(usingsrcDirstill works but logs a warning). serverDirdefaults tofalse, meaning no directory scanning happens until you explicitly enable it. Scanned routes, middleware, plugins, and tasks won't be registered otherwise.
Migration: Set serverDir in your Nitro config:
import { defineConfig } from "nitro";
export default defineConfig({
serverDir: "./server", // or "." to keep the v2 root layout
});Setting serverDir: true is a shortcut for "server". Paths are resolved relative to the project root.
#unenv Option Is Deprecated
The unenv config option (and nitro.options.unenv in hooks) is deprecated in favor of the alias, inject, polyfills and builtinModules options (unenv's polyfill and external map to polyfills and builtinModules). Presets passed to unenv still work, and a warning is logged.
export default defineConfig({
- unenv: {
- alias: { "my-module": "my-module/web" },
- external: ["cloudflare:workers"],
- },
+ alias: { "my-module": "my-module/web" },
+ builtinModules: ["cloudflare:workers"],
});Modules that pushed presets from hooks should update these options directly (e.g. nitro.options.builtinModules.push(...)). Values set in these options override the defaults added by deployment presets (e.g. Cloudflare and Deno map supported Node.js builtins to native modules).
unenv is no longer a dependency of Nitro. When a node: false build imports a Node.js built-in module that the target runtime does not support, unenv is resolved from your project and installed automatically (without prompting). Add it to your project dependencies if you import unenv directly.
#unstorage, db0 and ocache Are Installed On Demand
unstorage, db0 and ocache are no longer dependencies of Nitro. They are resolved from your project and installed on demand when used:
unstorage: when you importnitro/kv(ornitro/storage), configurekvmounts, or setcache.driverto"kv".db0: whenexperimental.databaseis enabled or you importnitro/database.ocache: when you importnitro/cache(defineCachedHandler,defineCachedFunction) or usecache/swrroute rules.
They are installed into your project dependencies without prompting. Add them yourself if you import unstorage, db0 or ocache directly.
#App Config Support Removed
Nitro v2 supported a bundled app config that allowed defining configurations in app.config.ts and accessing them at runtime via useAppConfig().
This feature has been removed.
Migration:
Use a regular .ts file in your server directory and import it directly.
#Route Rules
Route rule matching and handling moved to h3-rules. The rule shapes for redirect, proxy, cors, headers, cache, and swr are now defined by h3-rules, while isr, prerender, and static remain Nitro-specific rules.
The NitroRouteConfig and NitroRouteRules types are deprecated in favor of RouteRuleConfig and NormalizedRouteRules, re-exported from nitro/types:
- import { NitroRouteConfig, NitroRouteRules } from "nitropack/types"
+ import { RouteRuleConfig, NormalizedRouteRules } from "nitro/types"getRouteRules(event) is replaced by event.context.routeRules. To resolve the rules of another path, use getRouteRules(method, pathname).routeRules from nitro/app.
- import { getRouteRules } from "nitropack/runtime"
- const rules = getRouteRules(event)
+ import { getRouteRules } from "nitro/app"
+ const rules = event.context.routeRules
+ const { routeRules: blogRules } = getRouteRules("GET", "/blog/post")#Runtime Utilities
Runtime utilities have been moved to individual nitro/* subpath exports. Refer to docs for usage.
-- import { useStorage } from "nitropack/runtime/storage"
++ import { useKV } from "nitro/kv"| Utilities | Nitro v3 Import |
|---|---|
useKV (formerly useStorage) | nitro/kv |
defineCachedFunction, defineCachedHandler | nitro/cache |
useDatabase | nitro/database |
useRuntimeConfig | nitro/runtime-config |
defineTask, runTask | nitro/task |
useNitroApp, useNitroHooks, getRouteRules | nitro/app |
Note
useStorage has been renamed to useKV and nitro/storage to nitro/kv. The storage config option has been renamed to kv, and devStorage is replaced by kv inside $development (add it to $prerender too if you relied on devStorage while prerendering). The old names still work as deprecated aliases and will be removed in a future version.
#Cache Storage
Cached functions and handlers no longer use the storage layer by default. Entries are stored in an in-memory LRU storage, configurable with the new cache option (driver: "memory" | "fs" | "kv").
If you configured a cache storage mount point (for example storage: { cache: { driver: "redis" } }), cache entries keep being stored in it. If your cached functions, handlers or route rules use a custom base pointing to another mount point, or you mount the cache storage at runtime (useKV().mount(...) in a plugin), set cache: { driver: "kv" }.
#Internal Server Fetch
To make internal requests to your own server (the v2 useNitroApp().localFetch pattern), use the serverFetch utility exported from nitro. There is also a fetch export that routes absolute paths (starting with /) to the server and everything else to global fetch.
import { serverFetch } from "nitro";
const res = await serverFetch("/api/hello"); // returns a web Response#Plugins
The defineNitroPlugin utility has been renamed to definePlugin and is now imported from nitro.
-- export default defineNitroPlugin((nitroApp) => {
++ import { definePlugin } from "nitro"
++
++ export default definePlugin((nitroApp) => {#Optional Hooks
If you were using useNitroApp().hooks outside of Nitro plugins before, it might be undefined. Use new useNitroHooks() to guarantee having an instance.
import { useNitroHooks } from "nitro/app";
useNitroHooks().hook("request", (event) => {
/* ... */
});#Build Hooks Renamed
The rollup:before and rollup:reload build hooks run for all builders (Rollup, Rolldown and Vite), so they are renamed to build:config and build:reload. The build:config hook receives { builder, rollup } or { builder, rolldown } instead of the bare bundler config. With the Vite builder, builder is "vite" and the config is the one for the bundler Vite uses.
-- nitro.hooks.hook("rollup:before", (nitro, config) => {
-- config.plugins.push(myPlugin());
++ nitro.hooks.hook("build:config", (nitro, { builder, rollup, rolldown }) => {
++ const config = rollup ?? rolldown;
++ config.plugins.push(builder === "rollup" ? myRollupPlugin() : myPlugin());
});The old names still work as deprecated aliases (a warning is logged): rollup:before hooks are called from build:config with the bare config, and rollup:reload hooks are called by build:reload. Calling the old names with nitro.hooks.callHook() no longer runs any hooks.
#Error Handler
The defineNitroErrorHandler utility has been renamed to defineErrorHandler and is now imported from nitro.
-- export default defineNitroErrorHandler((error, event) => {
++ import { defineErrorHandler } from "nitro"
++
++ export default defineErrorHandler((error, event) => {#H3 v2
Nitro v3 upgrades to H3 v2, which includes API changes. All H3 utilities are imported from nitro/h3.
#Web Standards
H3 v2 is rewritten based on web standard primitives (URL, Headers, Request, and Response).
Access to event.node.{req,res} is only available in Node.js runtime. event.web is renamed to event.req (instance of web Request).
#Response Handling
You should always explicitly return the response body or throw an error:
-- import { send, sendRedirect, sendStream } from "nitro/h3"
-- send(event, value)
-- sendStream(event, stream)
-- sendRedirect(event, location, code)
++ import { redirect } from "nitro/h3"
++ return value
++ return stream
++ return redirect(location, code)Other changes:
sendError(event, error)→throw error(orthrow new HTTPError(...))sendNoContent(event)→return noContent()sendProxy(event, target)→return proxy(event, target)
#Request Body
Most body utilities can be replaced with native event.req methods:
-- import { readBody, readRawBody, readFormData } from "nitro/h3"
++ // Use native Request methods
++ const json = await event.req.json()
++ const text = await event.req.text()
++ const formData = await event.req.formData()
++ const stream = event.req.body#Headers
H3 now uses standard web Headers. Header values are always plain string (no null, undefined, or string[]).
-- import { getHeader, setHeader, getResponseStatus } from "nitro/h3"
-- getHeader(event, "x-foo")
-- setHeader(event, "x-foo", "bar")
++ event.req.headers.get("x-foo")
++ event.res.headers.set("x-foo", "bar")
++ event.res.status // instead of getResponseStatus(event)#Handler Utils
-- import { eventHandler, defineEventHandler } from "nitro/h3"
++ import { defineHandler } from "nitro"lazyEventHandler→defineLazyEventHandleruseBase→withBase
#Error Utils
-- import { createError, isError } from "nitro/h3"
++ import { HTTPError } from "nitro"
++ throw new HTTPError({ status: 404, message: "Not found" })
++ HTTPError.isError(error)#Node.js Utils
-- import { defineNodeListener, fromNodeMiddleware, toNodeListener } from "nitro/h3"
++ import { defineNodeHandler, fromNodeHandler, toNodeHandler } from "nitro/h3"#Preset Updates
Nitro presets have been updated for the latest compatibility.
Some (legacy) presets have been removed or renamed.
| Old Preset | New Preset |
|---|---|
node (Node.js middleware) | node_middleware (export changed to middleware) |
cloudflare, cloudflare_worker, cloudflare_module_legacy | cloudflare_module |
deno-server-legacy | deno_server with Deno v2 |
netlify-builder | netlify or netlify_edge |
vercel-edge | vercel with Fluid compute enabled |
azure, azure_functions | azure_swa |
firebase | firebase_app_hosting |
iis | iis_handler |
deno (Deno Deploy) | deno_deploy |
edgio | Discontinued |
cli | Removed due to lack of use |
service_worker | Removed due to instability |
Warning
The node and deno preset names still exist in v3, but they now point to different targets:
- In v2, the
nodepreset produced a reusable Node.js middleware. In v3,nodeis an alias ofnode_server, which builds a full standalone Node.js server. If you keeppreset: "node", you now get a standalone server. Usenode_middlewareto keep the v2 middleware output. - In v2, the
denopreset targeted Deno Deploy. In v3,denois an alias ofdeno_server, which builds a self-hosted Deno server. Usedeno_deployto keep deploying to Deno Deploy.
#AWS Amplify Default Runtime
The aws_amplify preset now defaults the compute runtime to nodejs22.x (previously nodejs20.x), as Node.js 20 reached end-of-life in April 2026.
Migration:
- Test your app on Node.js 22, or pin a runtime explicitly with
awsAmplify: { runtime: "nodejs24.x" }in your Nitro config.
#Cloudflare Dev and Vercel Observability Ignore Compatibility Date
Two preset features that were gated on compatibilityDate are now always enabled, so configs carried over from v2 with older dates keep them:
- The
cloudflare_module,cloudflare_durableandcloudflare_pagespresets always use thecloudflare-devpreset (Miniflare emulation with bindings) in development. Previously, acompatibilityDatebefore2025-07-13silently fell back to the genericnitro-devpreset. - The
vercelpreset always generates observability routes. Previously, they required acompatibilityDateof2025-07-15or later.
#Azure Functions v4 Programming Model
The azure_swa preset now deploys the server using the Azure Functions Node.js v4 programming model instead of function.json. It requires @azure/functions v4 in your project dependencies:
npm i @azure/functions@^4Migration:
- Add
@azure/functions@^4to your dependencies (Nitro offers to install it when missing). If you have v3 installed, update it. - To keep the legacy v3 programming model, set
azure: { functionsVersion: 3 }in your Nitro config.
#Vercel ISR Route Rules
In Nitro v2, the vercel presets translated swr, static and cache: false route rules into Vercel ISR config. In Nitro v3, only the isr route rule configures ISR on Vercel, and swr/cache rules use Nitro's built-in caching layer like on other platforms. The future.nativeSWR option, which opted in to this behavior, has been removed.
Migration:
Use the isr route rule where you relied on Vercel ISR, and remove future.nativeSWR from your config.
export default defineConfig({
-- future: { nativeSWR: true },
routeRules: {
-- "/blog/**": { swr: true },
-- "/api/realtime/**": { cache: false },
++ "/blog/**": { isr: true },
++ "/api/realtime/**": { isr: false },
},
});#Netlify Output Directories
The netlify and netlify_edge presets now write their output to the Netlify Frameworks API directories:
| Output | Before | After |
|---|---|---|
Server function (netlify) | .netlify/functions-internal | .netlify/v1/functions |
Edge function (netlify_edge) | .netlify/edge-functions | .netlify/v1/edge-functions |
Deploy config (netlify.config) | .netlify/deploy/v1/config.json | .netlify/v1/config.json |
The edge function config is now inlined in server.mjs instead of .netlify/edge-functions/manifest.json, and netlify.config is also applied by the netlify_edge preset.
Migration:
- Update CI scripts or cache paths that reference the old directories.
- When deploying a local build with
netlify deploy --no-build, remove leftovers from older builds in.netlify/functions-internaland.netlify/edge-functionsfirst.
#Cloudflare Bindings Access
In Nitro v2, Cloudflare environment variables and bindings were accessible via event.context.cloudflare.env.
In Nitro v3, the Cloudflare runtime context is attached to the request's runtime object instead.
Migration:
-- const { cloudflare } = event.context
-- const binding = cloudflare.env.MY_BINDING
++ const { env } = event.req.runtime.cloudflare
++ const binding = env.MY_BINDING#Cloudflare Email and Queue Hooks
The deprecated event field is removed from the cloudflare:email and cloudflare:queue runtime hooks. Use message and batch instead.
Migration:
-- nitroApp.hooks.hook("cloudflare:email", ({ event }) => {})
++ nitroApp.hooks.hook("cloudflare:email", ({ message }) => {})
-- nitroApp.hooks.hook("cloudflare:queue", ({ event }) => {})
++ nitroApp.hooks.hook("cloudflare:queue", ({ batch }) => {})#Cloudflare Exports
Earlier v3 prereleases experimentally supported exporting Durable Objects, Workflows and other Cloudflare classes from an exports.cloudflare.ts file (or the cloudflare.exports option). They are now exported from your server.ts file instead, and also work in local development. See Durable Objects.
++ export { MyWorkflow } from "./exports.cloudflare.ts";
++
++ export default {
++ fetch() {},
++ };#Vite Plugin Options
Earlier v3 prereleases accepted services, assetsImport and serverReload only as experimental.vite options passed to the nitro() Vite plugin. They moved to the top-level vite option, which can also be set under nitro in the Vite config or in nitro.config.ts. experimental.vite still works but is deprecated. The deprecated ServiceConfig type is removed in favor of ViteServiceConfig.
-- nitro({ experimental: { vite: { services: { api: { entry: "./api.ts" } } } } })
++ nitro({ vite: { services: { api: { entry: "./api.ts" } } } })#Removed Deprecated Options
Some options and exports that were deprecated or had no effect in earlier v3 prereleases are removed:
netlify.images: usenetlify.config.images.vercel.regions: it only applied to the removedvercel-edgepreset. Usevercel.functions.regions.prerender.retryandprerender.retryDelay: they had no effect in v3.useKVfromnitro/storage: import it fromnitro/kv.getBuildInfo(rootDir)fromnitro/builder: pass an options object,getBuildInfo({ rootDir }).