nitro review
Nitro 3.0.260610-beta turns a Vite project into a server application and then emits an artifact for Node, serverless, or edge deployment presets. Files become H3 request handlers, middleware, plugins, tasks, and websocket endpoints. Its runtime also includes route rules, response and function caches, key-value storage, runtime configuration, and prerendering. This beta moves prerender work into an isolate, builds Vite service entries from an explicit module graph, and makes development fetch errors match production behavior.
Nitro 3 beta took 46.4 seconds and 31 MB to install on our box, loaded through both module systems, bundled to 3 KB gzipped in the browser probe, and returned zero audit findings. Adopt it when Vite plus multiple deployment presets justify beta churn; use a smaller router for one ordinary Node service.
We installed it
| Install | ✓ · 46.4s | 22 packages on disk · 31 MB |
| Import | ✓ | ESM import works · require() works · ESM package with exports map |
| Browser | 3 KB | gzipped (7.4 KB minified), bundled with esbuild |
| Types | ✓ | TypeScript types bundled |
| Known vulns | 0 | 0 critical · 0 high · 0 moderate · 0 low (npm audit) |
Answers from our run
Does nitro install cleanly?
Yes. In a fresh container with an empty cache, npm install nitro finished in 46 seconds, leaving 22 packages and 31 MB on disk. npm audit reported no known vulnerabilities.
How much does nitro add to a browser bundle?
3 KB gzipped (7.4 KB minified) when the whole package is bundled for the browser with esbuild. Importing only part of it is usually smaller.
Does nitro work with both ESM and CommonJS?
Yes. Both import 'nitro' and require('nitro') worked in Node 22 in our run. The package is published as ESM with an exports map.
Does nitro include TypeScript types?
Yes, type declarations ship inside the package, so no @types install is needed.
nitro or h3: which should you use?
h3: Choose it for Nitro's web-standard handler model without the builder, presets, storage, and prerender layers. Nitro 3 beta took 46.4 seconds and 31 MB to install on our box, loaded through both module systems, bundled to 3 KB gzipped in the browser probe, and returned zero audit findings.
When should you not use nitro?
Your production dependency policy excludes prereleases; npm's latest Nitro package is explicitly 3.0.260610-beta
Use it if
- A Vite application needs API routes and server rendering in the same project, with one build pipeline owning both sides
- The same handler code must produce artifacts for Node and one or more documented serverless or edge providers
- Route rules, server plugins, prerendering, storage mounts, and response caching would otherwise require separate infrastructure packages
- The team is ready to test a version 3 beta and can run smoke tests against the actual deployment preset
- Your production dependency policy excludes prereleases; npm's latest Nitro package is explicitly 3.0.260610-beta
- A version 2 application cannot absorb changes to package names, handler imports, request APIs, errors, configuration, and deployment preset names
- The service is a single Node API with no Vite integration or cross-provider build requirement; Fastify or Hono has fewer framework layers
- CI is fixed to Node 18 or an early Node 20 image; Nitro 3 requires Node ^20.19.0 or at least 22.12.0
- The application requires one identical filesystem, cache, websocket, and binding model on every target; each deployment preset still inherits provider limits
Setup reality
We installed Nitro 3.0.260610-beta in a fresh Node 22 Bookworm container in 46.4 seconds. The result occupied 31 MB across 22 packages, while npm audit found zero known vulnerabilities at all 4 severity levels. Nitro declares 14 direct dependencies and 8 peers, is 4328 KB unpacked, and includes TypeScript declarations. The package requires Node ^20.19.0 or >=22.12.0.
The package declares ESM and an exports map. Both require() and ESM import worked on our Node 22.23.2 box. An import-all esbuild browser probe produced 7.4 KB minified and 3 KB gzipped. That number describes the exported surface the probe retained; Nitro's builders, routes, storage, and provider adapters are still server-side concerns. Install only the peers needed by the chosen Vite, Rollup, queue, XML, or deployment integration.
A Nitro 3 project needs nitro.config.ts plus a Vite plugin or a direct server entry. Values in .env and .env.local load during development, while production expects the hosting platform's environment configuration. Runtime overrides use NITRO_ names and only replace keys already declared in runtimeConfig. The Node preset writes .output/server/index.mjs, reads PORT or NITRO_PORT, and defaults to port 3000.
Preset choice affects more than packaging. Production cache and root storage default to memory, so data disappears at restart and remains private to each instance unless you mount a shared driver. Cached handlers drop request headers unless they are listed in varies, which can mix tenant or locale responses under one key. Cloudflare bindings, Vercel features, WebSocket upgrades, filesystem access, and cold starts need tests on the target platform. The current beta's Vercel websocket support is still described as internal testing.
Patterns
Attach Nitro to a Vite project enable-vite-server
// vite.config.ts
import { defineConfig } from 'vite'
import { nitro } from 'nitro/vite'
export default defineConfig({
plugins: [nitro()],
})
// nitro.config.ts
export default defineConfig({
serverDir: './server',
})Nitro 3 scans the configured serverDir for routes and plugins. Keep the config beside Vite so local and production builds resolve the same root.
Return JSON from a file route serve-json-route
// server/routes/api/status.get.ts
import { defineHandler } from 'nitro'
export default defineHandler(() => ({
ok: true,
version: 3,
}))The .get suffix limits this file to GET requests. A file under routes/api maps to the /api URL prefix.
Read a JSON request body parse-request-body
// server/routes/api/orders.post.ts
import { defineHandler, HTTPError } from 'nitro'
export default defineHandler(async (event) => {
const body = await event.req.json()
if (typeof body?.sku !== 'string') {
throw new HTTPError({ status: 400, message: 'sku is required' })
}
return saveOrder(body)
})Nitro 3 exposes the web Request through event.req. Validate body shape before passing it to storage or business code.
Use dynamic path parameters read-route-params
// server/routes/api/repos/[owner]/[name].get.ts
import { defineHandler } from 'nitro'
export default defineHandler((event) => {
const { owner, name } = event.context.params
return { owner, name }
})Bracketed folders create named path segments. Validate or encode the values before including them in a filesystem path or upstream URL.
Protect an API subtree with middleware guard-api-prefix
// server/middleware/auth.ts
import { defineHandler, HTTPError } from 'nitro'
export default defineHandler((event) => {
if (!event.url.pathname.startsWith('/api/private/')) return
const token = event.req.headers.get('authorization')
if (!token) throw new HTTPError({ status: 401, message: 'missing token' })
event.context.user = verifyToken(token)
})Middleware can run for every request, so narrow the path before doing authentication work. Returning a response stops the chain.
Cache a localized GET response cache-get-handler
import { defineCachedHandler } from 'nitro/cache'
export default defineCachedHandler(
async () => loadCatalog(),
{ maxAge: 300, swr: true, varies: ['accept-language'] },
)Cached handlers preserve only headers named in varies. Add authorization, host, or tenant headers when any of them changes the response.
Cache and invalidate one function result invalidate-cached-function
import { defineCachedFunction } from 'nitro/cache'
const loadPrice = defineCachedFunction(
async (sku: string) => database.price(sku),
{ name: 'price', maxAge: 60, getKey: (sku) => sku },
)
const first = await loadPrice('A-17')
await loadPrice.invalidate('A-17')invalidate() resolves the same key through getKey. Every argument that affects the result must appear in that key.
Declare cache, redirect, and prerender rules configure-route-rules
// nitro.config.ts
export default defineConfig({
routeRules: {
'/news/**': { swr: 600 },
'/api/live/**': { cache: false },
'/company': { prerender: true },
'/old/**': { redirect: { to: '/docs', status: 308 } },
},
})A preset may translate route rules into provider files or runtime behavior. Test the built target because provider capabilities differ.
Map production environment values into runtime config override-runtime-config
// nitro.config.ts
export default defineConfig({
runtimeConfig: {
billing: { endpoint: 'http://localhost:9000', token: '' },
},
})
// server route
import { useRuntimeConfig } from 'nitro/runtime-config'
const config = useRuntimeConfig()
// production environment
// NITRO_BILLING_ENDPOINT=https://billing.example.com
// NITRO_BILLING_TOKEN=secretProduction overrides only apply to keys declared in runtimeConfig. Development .env loading does not replace the host's production secret configuration.
Mount Redis instead of process memory mount-persistent-storage
// nitro.config.ts
export default defineConfig({
storage: {
shared: {
driver: 'redis',
url: process.env.REDIS_URL,
},
},
})
// route or plugin
import { useStorage } from 'nitro/storage'
await useStorage('shared').setItem('jobs:42', { state: 'queued' })Root and cache storage use memory in production unless configured otherwise. Memory disappears at restart and is separate in each instance.
Define a named server task register-background-task
// server/tasks/reports/daily.ts
import { defineTask } from 'nitro/task'
export default defineTask({
meta: { name: 'reports:daily', description: 'Build the daily report' },
async run({ payload }) {
const count = await buildReport(payload.date)
return { result: { count } }
},
})A task definition does not create a durable queue by itself. Choose a scheduler and retry policy that match the deployment provider.
Start the generated Node server build-node-output
NITRO_PRESET=node npx nitro build
PORT=8080 HOST=0.0.0.0 node .output/server/index.mjsThe Node preset defaults to port 3000 and handles SIGINT and SIGTERM. Ship the full .output directory, then exercise its health route before traffic moves.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| h3 | npm | Choose it for Nitro's web-standard handler model without the builder, presets, storage, and prerender layers |
| hono | npm | Choose it for a compact router that runs across several JavaScript runtimes with less build machinery |
| fastify | npm | Choose it for a Node-only service that values plugins, hooks, and schema-driven request validation |
| express | npm | Choose it when an established Node middleware stack matters more than portable deployment output |
More web backend guides
urllib3 · requests · ws · anyio · httpx · undici · the whole shelf →
How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.

