release · 

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, over 240 commits from more than 40 contributors went into this release, alongside many improvements across the H3 and 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 ↓

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

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 →

#Runtime mode in development

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

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 →

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

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 →

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

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 →

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

nitropack@2 nitro@beta nitro@rc Install size 69 MB 26 MB 4.7 MB External dependencies 338 20 6 Installed files 7,102 1,418 558

Measured with a fresh npm i on linux-x64 (nitropack@2.13.5 for v2).

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

#🧪 Zero-Config Testing with Vitest

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

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 →

See the Vitest example 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:

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

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

nitro.config.ts
import { defineConfig } from "nitro";

export default defineConfig({
  $env: {
    staging: { runtimeConfig: { apiBase: "https://staging.example.com" } },
  },
});
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.

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

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

Read more →

#🥟 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) instead of going through the node:http compatibility layer.

bun --bun nitro dev

WebSockets use the adapter of the runtime your code runs in, and 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 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 →

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

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 →

#🎁 And Much More

  • H3 v2 and rou3 v1: Nitro now runs on their stable releases.
  • KV storage: useStorage is now useKV from nitro/kv. The old name still works.
  • Cache storage: cached handlers and functions get their own cache 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.
  • Assets by URL: files loaded with new URL("./file.wasm", import.meta.url) are emitted with the build.
  • 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 and Vite 8 bundled dev mode.
  • And more: db0 tracing, prerender.ignoreStatusCodes, and startPreview()/deploy() from nitro/builder.

See the full changelog for every change.

#🏁 Get Started

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

npx --min-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. 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

BeforeAfter
rollup:before / rollup:reload hooksbuild:config / build:reload
experimental.vite.*vite.*
unenv optionalias, inject, polyfills, builtinModules
cloudflare_durable presetcloudflare_module
deno_deploy presetdeno_server
platform_sh presetupsun
exports.cloudflare.ts / cloudflare.exportsexports from server.ts
Vercel future.nativeSWRisr 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.


Please try the RC and report any issues. Stable is close! 🚀

Nitro  builds full-stack servers that deploy anywhere.