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:

Add explicit imports and update type and subpath imports.
Update your config: enable server directory scanning and review removed features.
Update handlers for H3 v2 API changes.

#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 --version and 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 rolldown

Without 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:

server/routes/index.ts
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 (use nitro/builder)
  • nitropack/runtime/* (use nitro/*)
  • nitropack/kit (removed)
  • nitropack/presets (removed)
  • nitropack/meta (use nitro/meta, which only exports version, runtimeDir and runtimeDependencies)

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 NitroModule from nitro/types instead of defineNitroModule from 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 srcDir option is deprecated in favor of serverDir (using srcDir still works but logs a warning).
  • serverDir defaults to false, 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:

nitro.config.ts
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 import nitro/kv (or nitro/storage), configure kv mounts, or set cache.driver to "kv".
  • db0: when experimental.database is enabled or you import nitro/database.
  • ocache: when you import nitro/cache (defineCachedHandler, defineCachedFunction) or use cache / swr route 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"
UtilitiesNitro v3 Import
useKV (formerly useStorage)nitro/kv
defineCachedFunction, defineCachedHandlernitro/cache
useDatabasenitro/database
useRuntimeConfignitro/runtime-config
defineTask, runTasknitro/task
useNitroApp, useNitroHooks, getRouteRulesnitro/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 (or throw 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 → defineLazyEventHandler
  • useBase → 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 PresetNew Preset
node (Node.js middleware)node_middleware (export changed to middleware)
cloudflare, cloudflare_worker, cloudflare_module_legacycloudflare_module
deno-server-legacydeno_server with Deno v2
netlify-buildernetlify or netlify_edge
vercel-edgevercel with Fluid compute enabled
azure, azure_functionsazure_swa
firebasefirebase_app_hosting
iisiis_handler
deno (Deno Deploy)deno_deploy
edgioDiscontinued
cliRemoved due to lack of use
service_workerRemoved due to instability

Warning

The node and deno preset names still exist in v3, but they now point to different targets:

  • In v2, the node preset produced a reusable Node.js middleware. In v3, node is an alias of node_server, which builds a full standalone Node.js server. If you keep preset: "node", you now get a standalone server. Use node_middleware to keep the v2 middleware output.
  • In v2, the deno preset targeted Deno Deploy. In v3, deno is an alias of deno_server, which builds a self-hosted Deno server. Use deno_deploy to 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_durable and cloudflare_pages presets always use the cloudflare-dev preset (Miniflare emulation with bindings) in development. Previously, a compatibilityDate before 2025-07-13 silently fell back to the generic nitro-dev preset.
  • The vercel preset always generates observability routes. Previously, they required a compatibilityDate of 2025-07-15 or 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@^4

Migration:

  • Add @azure/functions@^4 to 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.

nitro.config.ts
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:

OutputBeforeAfter
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-internal and .netlify/edge-functions first.

#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.

server.ts
++ 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.

vite.config.ts
-- 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: use netlify.config.images.
  • vercel.regions: it only applied to the removed vercel-edge preset. Use vercel.functions.regions.
  • prerender.retry and prerender.retryDelay: they had no effect in v3.
  • useKV from nitro/storage: import it from nitro/kv.
  • getBuildInfo(rootDir) from nitro/builder: pass an options object, getBuildInfo({ rootDir }).

Nitro  builds full-stack servers that deploy anywhere.