
# Nitro v3 Release Candidate is here!

> Nitro v3 RC is out with a stable API. 5× smaller install, runtime mode, Vitest support, and so much more.

Since the [last beta](https://github.com/nitrojs/nitro/releases/tag/v3.0.260903-beta), over 240 commits from more than 40 contributors went into this release, alongside many improvements across the [H3](https://github.com/h3js) and [UnJS](https://github.com/unjs) ecosystems that power Nitro. We added the last big features, tightened every corner of the API, and cleared out legacy code so that v3 stable can start clean. Here are the highlights.

[Get Started ↓](#-get-started)

## 📦 Runtime Mode

Nitro has always been a build tool for servers. Now you can **skip the build entirely** and use it as a runtime (experimental).

```sh
npx nitro serve
```

The new `nitro serve` command (alias `nitro start`) starts a production server straight from your sources, in any Nitro project, whatever builder is configured:

- **No build step.** Your sources run as they are, with nothing to compile ahead of time.
- **Production ready.** It handles WebSockets, scheduled tasks, public assets, `.env` files and graceful shutdown on `SIGINT`/`SIGTERM`.
- **Fast.** It uses the same compiled router in memory, and in our benchmark it served about as many requests per second as plain h3 on srvx.
- **Dev-friendly.** Run `nitro serve --dev` to watch your sources and restart on every change.

Your sources load in the main process, or in a Node.js worker when you use `buildPlugins`. TypeScript, JSX, aliases, `import.meta.*`, `raw`/`text`/`bytes` imports and WebAssembly all work the same as in a bundled build. Files are transformed on demand, and virtual modules are served from memory. `nitro preview` still serves only a build output.

[Read more →](/docs/cli#nitro-serve)

### Runtime mode in development

To use runtime mode with `nitro dev` too, set `builder: false`:

```ts [nitro.config.ts]
import { defineConfig } from "nitro";

export default defineConfig({
  builder: false,
});
```

Your sources then run in a worker that is replaced on every change. If `rolldown` isn't installed, `nitro dev` now falls back to runtime mode on its own. To start a new project in runtime mode, pick the **Runtime** template of `nitro create` (see Get Started below).

[Read more →](/config/build#builder)

### nitro fetch

Need one response instead of a running server? `nitro fetch` or `nitro curl` starts the server from your sources, sends a single request, prints the response and exits:

```sh
npx nitro fetch /api/hello
```

It supports common curl options (`-X`, `-H`, `-d`, `--json`, `-i`, ...). The body streams to stdout and the logs go to stderr, so you can pipe the output (`nitro fetch /api/users | jq`), and `4xx`/`5xx` responses exit with code `22`, which makes it handy for scripts and CI smoke tests. When `nitro dev` is running, the request goes to the dev server instead.

[Read more →](/docs/cli#nitro-fetch)

## 🧩 Build Plugins

The new `buildPlugins` option takes a single list of plugins that works with every builder: `rollup`, `rolldown`, `vite`, and even in runtime mode.

```ts [nitro.config.ts]
import { defineConfig } from "nitro";

export default defineConfig({
  buildPlugins: [
    {
      name: "my-virtual-module",
      resolveId: {
        filter: { id: /^virtual:my-module$/ },
        handler: (id) => `\0${id}`,
      },
      load: {
        filter: { id: /^\0virtual:my-module$/ },
        handler: () => `export default "Hello!"`,
      },
    },
  ],
});
```

Plugins use the familiar Rollup plugin interface. The `resolveId`, `load` and `transform` hooks with a `filter` work with every builder. Nitro modules can add their own with `nitro.options.buildPlugins.push(plugin)`.

[Read more →](/config/build#buildplugins)

## 🪶 5× Smaller Install

Nitro now has only **6 direct dependencies**. Builders, polyfills and drivers (`rolldown`, `unenv`, `unstorage`, `db0`) are installed on demand, only when your project needs them.

Compared to Nitro v2 (`nitropack`), the install is now about **15× smaller**, with 6 external dependencies instead of 338 and under 600 installed files instead of over 7,000.

<svg viewBox="0 0 768 160" role="img" aria-label="Install size: nitropack@2 69 MB, nitro@beta 26 MB, nitro@rc 4.7 MB. External dependencies: nitropack@2 338, nitro@beta 20, nitro@rc 6. Installed files: nitropack@2 7,102, nitro@beta 1,418, nitro@rc 558." style="width:100%;height:auto;font-family:inherit;font-size:14px">
  <text x="0" y="54" style="fill:var(--muted-foreground,currentColor)">nitropack@2</text>
  <text x="0" y="98" style="fill:var(--muted-foreground,currentColor)">nitro@beta</text>
  <text x="0" y="142" style="fill:var(--muted-foreground,currentColor)">nitro@rc</text>
  <line x1="320" y1="4" x2="320" y2="152" style="stroke:var(--muted-foreground,#888);opacity:.25" />
  <line x1="544" y1="4" x2="544" y2="152" style="stroke:var(--muted-foreground,#888);opacity:.25" />
  <text x="108" y="18" style="fill:var(--foreground,currentColor);font-weight:600">Install size</text>
  <rect x="108" y="40" width="140" height="18" rx="4" style="fill:var(--muted-foreground,#888);opacity:.35" />
  <text x="256" y="54" style="fill:var(--foreground,currentColor)">69 MB</text>
  <rect x="108" y="84" width="53" height="18" rx="4" style="fill:var(--muted-foreground,#888);opacity:.35" />
  <text x="169" y="98" style="fill:var(--foreground,currentColor)">26 MB</text>
  <rect x="108" y="128" width="10" height="18" rx="4" style="fill:var(--brand,#e0427f)" />
  <text x="126" y="142" style="fill:var(--foreground,currentColor);font-weight:600">4.7 MB</text>
  <text x="332" y="18" style="fill:var(--foreground,currentColor);font-weight:600">External dependencies</text>
  <rect x="332" y="40" width="140" height="18" rx="4" style="fill:var(--muted-foreground,#888);opacity:.35" />
  <text x="480" y="54" style="fill:var(--foreground,currentColor)">338</text>
  <rect x="332" y="84" width="8" height="18" rx="4" style="fill:var(--muted-foreground,#888);opacity:.35" />
  <text x="348" y="98" style="fill:var(--foreground,currentColor)">20</text>
  <rect x="332" y="128" width="3" height="18" rx="1.5" style="fill:var(--brand,#e0427f)" />
  <text x="343" y="142" style="fill:var(--foreground,currentColor);font-weight:600">6</text>
  <text x="556" y="18" style="fill:var(--foreground,currentColor);font-weight:600">Installed files</text>
  <rect x="556" y="40" width="140" height="18" rx="4" style="fill:var(--muted-foreground,#888);opacity:.35" />
  <text x="704" y="54" style="fill:var(--foreground,currentColor)">7,102</text>
  <rect x="556" y="84" width="28" height="18" rx="4" style="fill:var(--muted-foreground,#888);opacity:.35" />
  <text x="592" y="98" style="fill:var(--foreground,currentColor)">1,418</text>
  <rect x="556" y="128" width="11" height="18" rx="4" style="fill:var(--brand,#e0427f)" />
  <text x="575" y="142" style="fill:var(--foreground,currentColor);font-weight:600">558</text>
</svg>

<sub>Measured with a fresh `npm i` on linux-x64 (`nitropack@2.13.5` for v2).</sub>

The dev error page is now built in. Nitro still has **zero peer dependencies**. Frameworks can pass their own copy of any optional package with the new [`deps`](/config/build#deps) option.

## 🧪 Zero-Config Testing with Vitest

Add `nitro()` to your Vite config and run `vitest`. That's it.

```ts [api.test.ts]
import { expect, test } from "vitest";
import { serverFetch } from "nitro";

test("GET /api/hello", async () => {
  const res = await serverFetch("/api/hello");
  expect(await res.json()).toEqual({ hello: "world" });
});
```

Tests run inside a `nitro` environment, where routes, plugins, runtime config and KV work exactly as in your server. You don't need to start a dev server. Each test file gets its own app, `vi.mock` works, and watch mode reruns tests when you change a route.

[Read more →](/docs/testing)

See the [Vitest example](/examples/vitest) for a complete project.

## 🔌 Server Entry Options

A `server.ts` can now configure the server it runs on. Wrap the default export with `defineServerEntry()` to get typed options:

```ts [server.ts]
import { defineServerEntry } from "nitro";

export default defineServerEntry({
  port: 8080,
  maxRequestBodySize: 1024 * 1024,
  fetch(req) {
    return new Response("Hello from server entry!");
  },
});
```

On the Node.js, Bun and Deno presets, these options go straight to [srvx](https://srvx.h3.dev/): `port`, `hostname`, `reusePort`, `protocol`, `tls`, `silent`, `gracefulShutdown`, `maxRequestBodySize`, `trustProxy`, and the runtime-specific `node`, `bun` and `deno` settings. Environment variables such as `NITRO_PORT` and `NITRO_HOST` still take precedence, so you can keep configuring the server at runtime.

[Read more →](/docs/server-entry#server-options)

## 🏷️ Custom Environments

Nitro already applied `$development` and `$production` config overrides. Now you can pick any environment name with the new `--envName` flag of `nitro dev`, `nitro build`, `nitro serve` and `nitro deploy`:

```ts [nitro.config.ts]
import { defineConfig } from "nitro";

export default defineConfig({
  $env: {
    staging: { runtimeConfig: { apiBase: "https://staging.example.com" } },
  },
});
```

```sh
npx nitro build --envName staging
```

Server code can read the name with `import.meta.envName`, replaced at build time with every builder. Prerendered routes use the same name, so they get the same overrides as the server.

```ts [routes/env.ts]
import { defineHandler } from "nitro";

export default defineHandler(() => ({ env: import.meta.envName })); // "staging"
```

[Read more →](/docs/configuration#environment-specific-config)

## 🥟 Runtime-native dev server

`nitro dev` now runs natively on Bun and Deno. The dev server listens with the runtime's own server (`Bun.serve()` or `Deno.serve()` through [srvx](https://srvx.h3.dev/)) instead of going through the `node:http` compatibility layer.

```sh
bun --bun nitro dev
```

WebSockets use the adapter of the runtime your code runs in, and [`devProxy`](/config/dev#devproxy) works too: HTTP requests are proxied with `fetch` and WebSockets with the crossws proxy. On Node.js, nothing changes.

## ▲ Vercel

- **Explicit ISR.** Only the [`isr`](/deploy/providers/vercel#on-demand-incremental-static-regeneration-isr) route rule turns on ISR. `swr` and `cache` rules use Nitro's built-in cache, like on every other platform.
- **Query params.** With `allowQuery`, those params now reach your handler too.
- **Same routing as your app.** The Vercel routing config is built with rou3, so method-scoped rules, header-only rules and `redirect: false` exceptions match at runtime.
- **Runtime from `package.json`.** The function runtime is picked from `engines.node` or `engines.bun`, the same way Vercel does it.
- **`waitUntil` and observability.** `event.waitUntil()` works on the Node.js runtime, and observability routes are always generated.

[Read more →](/deploy/providers/vercel)

## ☁️ Local Cloudflare

Durable Objects, Workflows and RPC service bindings now work in `nitro dev` and `vite dev`, with hot reload. Export them from `server.ts` and declare them in your Wrangler config:

```ts [server.ts]
import { DurableObject } from "cloudflare:workers";

export class Counter extends DurableObject {
  async fetch() {
    const count = ((await this.ctx.storage.get<number>("count")) || 0) + 1;
    await this.ctx.storage.put("count", count);
    return Response.json({ count });
  }
}

export default { fetch() {} };
```

Bindings and `waitUntil` also work in dev. The `cloudflare_durable` preset is merged into `cloudflare_module`: add a `$DurableObject` binding, and WebSocket upgrades go to a shared Durable Object with pub/sub and hibernation.

[Read more →](/deploy/providers/cloudflare#durable-objects)

## 🎁 And Much More

- **H3 v2 and rou3 v1:** Nitro now runs on their stable releases.
- **KV storage:** `useStorage` is now [`useKV`](/docs/storage) from `nitro/kv`. The old name still works.
- **Cache storage:** cached handlers and functions get their own [`cache`](/docs/cache#cache-storage) storage.
- **Async plugins:** `definePlugin(async (nitroApp) => {})` works on every runtime.
- **HTTP:** `QUERY` method routes, `405` with an `Allow` header, and `HEAD` served by `GET` routes.
- **Middleware:** now runs on cached routes too.
- **Faster Node.js:** the global `Response` is srvx's [`FastResponse`](/config/features#fastresponse).
- **Assets by URL:** files loaded with `new URL("./file.wasm", import.meta.url)` are [emitted with the build](/docs/assets#files-referenced-by-url).
- **tsconfig `paths`:** resolve with every builder, in dev and production.
- **Platform routing:** Vercel, Netlify, Cloudflare Pages and EdgeOne configs use rou3, so they match your app.
- **Dev:** Chrome DevTools workspaces, Unix sockets, [`devServer.drain`](/config/dev#devserver) and Vite 8 bundled dev mode.
- **And more:** `db0` tracing, `prerender.ignoreStatusCodes`, and `startPreview()`/`deploy()` from `nitro/builder`.

See the [full changelog](https://github.com/nitrojs/nitro/releases/tag/v3.0.261029-rc) for every change.

## 🏁 Get Started

Nitro now has a built-in `nitro create` command, which replaces `create-nitro-app`:

::code-group
```sh [npm]
npx --min-release-age=0 nitro@latest create
```
```sh [pnpm]
pnpm dlx --config.minimum-release-age=0 nitro@latest create
```
::

::note
The `--min-release-age=0` (npm) and `--config.minimum-release-age=0` (pnpm) flags skip any minimum release age you have configured, so you get the RC on release day.
::

Pick a template:

- **Vite:** a full-stack app with a frontend.
- **Rolldown:** an API server, bundled for production.
- **Runtime:** an API server with no build step, using runtime mode (experimental).

## ⬆️ Upgrading from Beta

Most beta apps upgrade without changes. Check these first.

**Requirements**

- Node.js `>=22.22.0`.
- Add your builder (`rolldown`, `rollup` or `vite`) to `devDependencies`.

**Behavior changes**

- Cache entries are stored in memory by default. Set `cache: { driver: "kv" }` to keep using KV storage.
- Catch-all routes like `routes/docs/[...slug].ts` also match `/docs`.
- Dotted suffixes in scanned file names must be a method (`.get`), an environment (`.dev`) or a known file extension (`.xml`, `.json`, ...). Others, like `routes/hello.world.ts`, [fail the build](/docs/routing#reserved-suffixes). Rename the file or add the suffix to `scanSuffixes`.
- Hooks in your config run after the preset's hooks instead of replacing them.
- Node.js presets use srvx `FastResponse` as the global `Response`. Set `features: { fastResponse: false }` to opt out.
- With `rollup` and `rolldown`, tsconfig `paths` that point package names to source files now bundle those sources.

**Renamed**

| Before                                         | After                                            |
| ---------------------------------------------- | ------------------------------------------------ |
| `rollup:before` / `rollup:reload` hooks        | `build:config` / `build:reload`                  |
| `experimental.vite.*`                          | `vite.*`                                         |
| `unenv` option                                 | `alias`, `inject`, `polyfills`, `builtinModules` |
| `cloudflare_durable` preset                    | `cloudflare_module`                              |
| `deno_deploy` preset                           | `deno_server`                                    |
| `platform_sh` preset                           | `upsun`                                          |
| `exports.cloudflare.ts` / `cloudflare.exports` | exports from `server.ts`                         |
| Vercel `future.nativeSWR`                      | `isr` route rule                                 |

**Removed**

- `winterjs` preset.
- `netlify.images`, `vercel.regions`, `prerender.retry` and `prerender.retryDelay` options.
- `event` field of the `cloudflare:email` and `cloudflare:queue` hooks.

**Preset output**

- Netlify output moved to `.netlify/v1`.
- Azure uses the Functions v4 model by default.
- Cloudflare Pages and EdgeOne no longer prefix `baseURL` onto relative `redirect` targets.

For more details, see the [migration guide](/docs/migration).

---

Please try the RC and [report any issues](https://github.com/nitrojs/nitro/issues). Stable is close! 🚀

- [GitHub](https://github.com/nitrojs/nitro): Issues and discussions
- [Discord](https://discord.nitro.build): Chat with the community
