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

1. [Rename the `nitropack` dependency to `nitro`](#nitropack-is-renamed-to-nitro) and upgrade to [Node.js 20+](#minimum-supported-nodejs-version-20).
2. [Add explicit imports](#auto-imports-are-removed) and update [type](#type-imports) and [subpath](#changed-nitro-subpath-imports) imports.
3. Update your config: [enable server directory scanning](#server-directory-scanning-is-opt-in) and review [removed features](#app-config-support-removed).
4. Review [route rules](#route-rules) and [runtime utilities](#runtime-utilities).
5. Update handlers for [H3 v2](#h3-v2) API changes.
6. Check your [deployment preset](#preset-updates) and [platform-specific access](#cloudflare-bindings-access).

## `nitropack` Is Renamed to `nitro`

The NPM package [nitropack](https://www.npmjs.com/package/nitropack) (v2) has been renamed to [nitro](https://www.npmjs.com/package/nitro) (v3).

**Migration:** Update the `nitropack` dependency to `nitro` in `package.json`:

::code-group
```diff [release channel]
{
  "dependencies": {
--    "nitropack": "latest"
++    "nitro": "latest"
  }
}
```
```diff [nightly channel]
{
  "dependencies": {
--    "nitropack": "latest"
++    "nitro": "npm:nitro-nightly@latest"
  }
}
```
::

See the [nightly channel](/docs/nightly) guide for more about nightly builds.

**Migration:** Search your codebase and rename all instances of `nitropack` to `nitro`:

```diff
-- 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](https://nodejs.org/en/about/previous-releases).

Please upgrade to the [latest LTS](https://nodejs.org/en/download) 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`](/config#builder)), 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:

:pm-install{name="-D rolldown"}

Without `rolldown` installed, `nitro dev` runs the server sources without a builder ([`builder: false`](/config#builder)).

## 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](https://github.com/unjs/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:

```ts
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:

```ts [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`:

```diff
-- 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)

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:

```ts [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`](/config#alias), [`inject`](/config#inject), [`polyfills`](/config#polyfills) and [`builtinModules`](/config#builtinmodules) options (unenv's `polyfill` and `external` map to `polyfills` and `builtinModules`). Presets passed to `unenv` still work, and a warning is logged.

```diff
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`](/config#node) build imports a Node.js built-in module that the target runtime does not support, `unenv` is resolved from your project and installed on demand. Add it to your project dependencies if you import `unenv` 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](https://github.com/h3js/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`:

```diff
- import { NitroRouteConfig, NitroRouteRules } from "nitropack/types"
+ import { RouteRuleConfig, NormalizedRouteRules } from "nitro/types"
```

## Runtime Utilities

Runtime utilities have been moved to individual `nitro/*` subpath exports. Refer to docs for usage.

```diff
-- 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`](/config#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`.

```ts
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`.

```diff
-- 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.

```ts
import { useNitroHooks } from "nitro/app";

useNitroHooks().hook("request", (event) => {
  /* ... */
});
```

## Error Handler

The `defineNitroErrorHandler` utility has been renamed to `defineErrorHandler` and is now imported from `nitro`.

```diff
-- export default defineNitroErrorHandler((error, event) => {
++ import { defineErrorHandler } from "nitro"
++
++ export default defineErrorHandler((error, event) => {
```

## H3 v2

Nitro v3 upgrades to [H3 v2](https://h3.dev), which includes API changes. All H3 utilities are imported from `nitro/h3`.

### Web Standards

H3 v2 is rewritten based on web standard primitives ([`URL`](https://developer.mozilla.org/en-US/docs/Web/API/URL), [`Headers`](https://developer.mozilla.org/en-US/docs/Web/API/Headers), [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request), and [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/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`](https://developer.mozilla.org/en-US/docs/Web/API/Request)).

### Response Handling

You should always explicitly **return** the response body or **throw** an error:

```diff
-- 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:

```diff
-- 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`](https://developer.mozilla.org/en-US/docs/Web/API/Headers). Header values are always plain `string` (no `null`, `undefined`, or `string[]`).

```diff
-- 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

```diff
-- import { eventHandler, defineEventHandler } from "nitro/h3"
++ import { defineHandler } from "nitro"
```

- `lazyEventHandler` → `defineLazyEventHandler`
- `useBase` → `withBase`

### Error Utils

```diff
-- import { createError, isError } from "nitro/h3"
++ import { HTTPError } from "nitro"
++ throw new HTTPError({ status: 404, message: "Not found" })
++ HTTPError.isError(error)
```

### Node.js Utils

```diff
-- 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 `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.
::

## Azure Functions v4 Programming Model

The `azure_swa` preset now deploys the server using the Azure Functions [Node.js v4 programming model](https://learn.microsoft.com/en-us/azure/azure-functions/functions-reference-node?pivots=nodejs-model-v4) instead of `function.json`. It requires `@azure/functions` v4 in your project dependencies:

:pm-install{name="@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.

## 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:**

```diff
-- const { cloudflare } = event.context
-- const binding = cloudflare.env.MY_BINDING
++ const { env } = event.req.runtime.cloudflare
++ const binding = env.MY_BINDING
```
