references/advanced-migration.md
---
name: migration
description: Nitro v2 to v3 migration - package rename, subpath imports, H3 v2 API, and preset changes
---
# Migration: Nitro v2 → v3
Nitro v3 has intentional breaking changes. This is the highest-value reference for agents whose training data assumes `nitropack` v2 / H3 v1 patterns.
## Package & imports
The `nitropack` package is renamed to `nitro`.
```diff
- import { defineNitroConfig } from "nitropack/config"
+ import { defineConfig } from "nitro"
```
Runtime utils moved to dedicated `nitro/*` subpaths:
| Capability | v3 import |
|---|---|
| Handlers, config, plugins, errors | `nitro` (`defineHandler`, `defineConfig`, `definePlugin`, `defineErrorHandler`, `HTTPError`, `defineWebSocketHandler`, `defineRouteMeta`, `defineServerEntry`, `serverFetch`) |
| KV storage | `nitro/kv` (`useKV`, formerly `useStorage` from `nitro/storage`) |
| Cache | `nitro/cache` (`defineCachedHandler`, `defineCachedFunction`) |
| Runtime config | `nitro/runtime-config` (`useRuntimeConfig`) |
| Database | `nitro/database` (`useDatabase`) |
| Tasks | `nitro/task` (`defineTask`, `runTask`) |
| App / hooks | `nitro/app` (`useNitroApp`, `useNitroHooks`, `getRouteRules`) |
| H3 utilities | `nitro/h3` |
| Types | `nitro/types` |
| Vite plugin | `nitro/vite` |
Removed: `nitropack/kit`, `nitropack/presets`, `nitropack/core` (use `nitro/builder`). Use `NitroModule` from `nitro/types` instead of `defineNitroModule`.
Other renames:
- `defineNitroPlugin` → `definePlugin`
- `defineNitroConfig` → `defineConfig`
- `defineNitroErrorHandler` → `defineErrorHandler`
- Node.js minimum is now **20**.
- App config (`app.config.ts` + `useAppConfig()`) was **removed** — import a regular `.ts` module instead.
## Auto-imports are removed
v2 auto-imported `defineEventHandler`, `useStorage`, `useRuntimeConfig`, `defineCachedFunction`, `defineNitroPlugin`, `#imports`, and your `server/utils/`. **All gone in v3** — the `imports` option, `#imports`, and `nitro-imports.d.ts` no longer exist. Add explicit imports everywhere, including relative imports from your own `utils/`:
```ts
import { defineHandler, definePlugin, defineErrorHandler, HTTPError, serverFetch } 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";
```
## Server directory scanning is opt-in
`srcDir` is deprecated for `serverDir`, which defaults to **`false`** — nothing (`routes/`, `api/`, `middleware/`, `plugins/`, `tasks/`) is scanned until you set it. Set `serverDir: "./server"` (or `"."` for the v2 root layout; `true` = `"server"`).
## Storage & cache config renames
- `useStorage` → `useKV` (`nitro/storage` → `nitro/kv`).
- `storage` config option → `kv`; `devStorage` → `kv` inside `$development` (and `$prerender`).
- Cache: `swr` now defaults to `false` (was `true`); persist cache by mounting a `cache` point under `kv`.
## Internal server fetch
`useNitroApp().localFetch` → `serverFetch` from `nitro` (returns a web `Response`). There is also a `fetch` export routing absolute (`/`) paths to the server.
## Route rule types
`NitroRouteConfig`/`NitroRouteRules` are deprecated for `RouteRuleConfig`/`RouteRules` (from `nitro/types`). `redirect`, `proxy`, `cors`, `headers`, `cache`, `swr` are now defined by [h3-rules](https://github.com/h3js/h3-rules); `isr`, `prerender`, `static` remain Nitro-specific. There is no `auth`/`basicAuth` route rule.
## H3 v2 API
H3 v2 is built on web standards (`URL`, `Headers`, `Request`, `Response`). All H3 utils import from `nitro/h3`.
### Return / throw, don't `send`
```diff
- import { send, sendRedirect, sendStream } from "nitro/h3"
- send(event, value); sendStream(event, stream); sendRedirect(event, loc, code)
+ import { redirect } from "nitro/h3"
+ return value
+ return stream
+ return redirect(event, loc, code)
```
Also: `sendError` → `throw createError`/`HTTPError`; `sendNoContent` → `return noContent(event)`; `sendProxy` → `return proxy(event, target)`.
### Event shape
`event.web` → `event.req` (a web `Request`). `event.node.{req,res}` only exists on Node.
```diff
- const body = await readBody(event)
+ const body = await event.req.json() // or .text() / .formData(); event.req.body for the stream
```
### Headers (always plain strings)
```diff
- getHeader(event, "x-foo"); setHeader(event, "x-foo", "bar"); getResponseStatus(event)
+ event.req.headers.get("x-foo")
+ event.res.headers.set("x-foo", "bar")
+ event.res.status
```
### Handlers & errors
```diff
- import { eventHandler, defineEventHandler, createError } from "nitro/h3"
+ import { defineHandler, HTTPError } from "nitro"
+ throw new HTTPError({ status: 404, message: "Not found" })
+ HTTPError.isError(error)
```
`lazyEventHandler` → `defineLazyEventHandler`; `useBase` → `withBase`. Node utils: `defineNodeListener`/`toNodeListener`/`fromNodeMiddleware` → `defineNodeHandler`/`toNodeHandler`/`fromNodeHandler`.
> `defineEventHandler`/`createError` still work in many places (re-exported), but prefer the v3 names: `defineHandler` and `HTTPError`.
## Cloudflare bindings
```diff
- const binding = event.context.cloudflare.env.MY_BINDING
+ const { env } = event.req.runtime.cloudflare
+ const binding = env.MY_BINDING
```
## Preset renames
| v2 | v3 |
|---|---|
| `node` | `node_middleware` (export is now `middleware`) |
| `cloudflare`, `cloudflare_worker`, `cloudflare_module_legacy` | `cloudflare_module` |
| `deno-server-legacy` / `deno` | `deno_server` (Deno v2) / `deno_deploy` |
| `netlify-builder` | `netlify` or `netlify_edge` |
| `vercel-edge` | `vercel` (Fluid compute) |
| `azure`, `azure_functions` | `azure_swa` |
| `firebase` | `firebase_app_hosting` |
| `iis` | `iis_handler` |
| `edgio`, `cli`, `service_worker` | removed/discontinued |
## Hooks
If you accessed `useNitroApp().hooks` outside a plugin it may be undefined — use `useNitroHooks()` to guarantee an instance.
## Key Points
- Replace `nitropack` with `nitro` and split imports into `nitro/*` subpaths.
- **Auto-imports are gone** — import everything explicitly; set `serverDir` or nothing is scanned.
- `useStorage`→`useKV` (`nitro/kv`), `storage`→`kv`, `devStorage`→`$development.kv`.
- Handlers **return**/**throw**; use `event.req.json()` and web `Headers` (no `send*`/`readBody`/`getHeader`).
- Use `HTTPError` instead of `createError`; `definePlugin`/`defineHandler`/`defineConfig`/`defineErrorHandler` instead of v2 names.
- Cloudflare bindings moved to `event.req.runtime.cloudflare.env`.
- Many presets were renamed/removed — update `preset` accordingly.
<!--
Source references:
- https://nitro.build/docs/migration
- https://h3.dev
-->
references/core-assets.md
---
name: assets
description: Public assets served to clients and server assets bundled for runtime access in Nitro
---
# Assets
Nitro handles three asset kinds: **public assets** served directly to clients, **imported files** inlined into the bundle, and **server assets** bundled into the server for programmatic access via the storage layer.
## Public assets
Files in `public/` are served automatically at the matching URL.
```
public/
image.png -> /image.png
robots.txt -> /robots.txt
```
- Served with automatic `ETag` / `Last-Modified` and `304 Not Modified` support.
- In production, `public/` is copied to `.output/public/` and a metadata manifest is embedded for fast lookups + caching headers.
### Custom public directories
```ts [nitro.config.ts]
import { defineConfig } from "nitro";
export default defineConfig({
publicAssets: [
{
baseURL: "build", // served under /build/
dir: "public/build", // source on disk
maxAge: 3600, // Cache-Control: public, max-age=3600, immutable
},
],
});
```
Other entry options: `dir` (relative to `rootDir`), `fallthrough` (continue to handlers when not found; defaults `true` for root, `false` otherwise) and `ignore`. `maxAge` only applies when `fallthrough` is `false`.
### Pre-compression
Generate gzip/brotli/zstd variants at build time, served based on `Accept-Encoding`:
```ts [nitro.config.ts]
export default defineConfig({
compressPublicAssets: true, // or { gzip: true, brotli: true, zstd: false }
});
```
Only compressible MIME types ≥ 1 KB are compressed (`.map` files excluded; zstd needs Node 22+ at build time).
## Importing files
Inline any file into the server bundle with an import attribute (base64-encoded for binary). Prefer server assets for larger files.
```ts
import logo from "./logo.png" with { type: "bytes" }; // Uint8Array
import readme from "./README.md" with { type: "text" }; // string
// or the raw: prefix
import logo2 from "raw:./logo.png"; // Uint8Array
import readme2 from "raw:./README.md"; // string
```
## Server assets
Files in `assets/` (relative to `serverDir` when set, else project root) are bundled into the server and read via the storage layer at the `assets:server` mount point (only included in the bundle when accessed through `useKV`).
```
assets/
data.json
templates/welcome.html
```
```ts [routes/index.ts]
import { defineHandler } from "nitro";
import { useKV } from "nitro/kv";
export default defineHandler(async () => {
const serverAssets = useKV("assets:server");
const keys = await serverAssets.getKeys();
const data = await serverAssets.getItem("data.json");
const meta = await serverAssets.getMeta("data.json"); // { type, etag, mtime }
return { keys, data, meta };
});
```
### Custom server asset directories
```ts [nitro.config.ts]
import { defineConfig } from "nitro";
export default defineConfig({
serverAssets: [
{ baseName: "templates", dir: "./templates" },
],
});
```
Access via the `assets:templates` mount:
```ts
const html = await useKV("assets:templates").getItem("email.html");
```
Entry options: `baseName`, `dir` (relative to `rootDir`), `pattern` (default `**/*`), `ignore`.
## Key Points
- `public/` → served to clients with ETag/compression; `assets/` → bundled, read via `useKV("assets:server")`.
- Server assets are only bundled if referenced through `useKV` (v2 `useStorage`).
- In dev, server assets read from the filesystem; in production they are inlined with precomputed metadata.
- Use `publicAssets[].maxAge` and `compressPublicAssets` to offload caching/compression without a CDN.
<!--
Source references:
- https://nitro.build/docs/assets
- https://nitro.build/docs/storage#server-assets
-->
references/core-cache.md
---
name: cache
description: Cached event handlers, cached functions, SWR, options and invalidation in Nitro
---
# Caching
Nitro's cache layer (powered by [ocache](https://ocache.unjs.io)) builds on the storage layer. Import helpers from `nitro/cache`.
> **v3 note:** `swr` now defaults to **`false`** (v2 defaulted to `true`), and `maxAge` defaults to **1 second** — always set both explicitly for anything you want cached.
## Cached handlers
`defineCachedHandler` works like `defineHandler` plus a cache-options argument.
```ts [routes/cached.ts]
import { defineCachedHandler } from "nitro/cache";
export default defineCachedHandler((event) => {
return "I am cached for an hour";
}, { maxAge: 60 * 60 });
```
Behavior:
- Only `GET`/`HEAD` are cached (stored under **separate** entries); other methods and requests with a `Range` header bypass and call the handler.
- Auto-manages `etag` (weak, from body hash), `cache-control` (synthesized from the enforced lifetime), `vary`, and `x-cache` (`HIT`/`STALE`/`REVALIDATED`/`MISS`) headers, plus `304 Not Modified` for conditional requests.
- Concurrent requests for the same key are deduplicated (handler runs once), bounded by `maxResolveTime` (30s default).
- `last-modified` is never synthesized; a handler-set `cache-control` is never overwritten.
### What the handler can see
On cacheable requests the handler only receives request data covered by the cache key; everything else is stripped so it can't render output the key doesn't distinguish:
- **Headers** are stripped unless in `varies` (includes `if-none-match`/`if-modified-since`, handled by Nitro).
- **Query params** are stripped unless in `allowQuery` (default: none; `/page` and `/page?a=1` share one entry).
- **Cookies** are stripped unless in `allowCookies`; `set-cookie` is dropped from cached responses.
- **`authorization`/`proxy-authorization`** are stripped unless `allowAuthorization` is set.
- **`host`** is rewritten to the resolved origin host (part of the key), so multi-host handlers get one entry per host.
Use `shouldBypassCache` for requests that must reach the handler untouched (not stored).
## Cached functions
Cache any async function (e.g. an upstream API call) and reuse it across handlers.
```ts [routes/api/stars/[...repo].ts]
import { defineHandler, type H3Event } from "nitro";
import { defineCachedFunction } from "nitro/cache";
const cachedGHStars = defineCachedFunction(async (repo: string) => {
const data = await fetch(`https://api.github.com/repos/${repo}`).then((r) => r.json());
return data.stargazers_count;
}, {
maxAge: 60 * 60,
name: "ghStars",
getKey: (repo: string) => repo,
});
export default defineHandler(async (event) => {
const { repo } = event.context.params;
const stars = await cachedGHStars(repo).catch(() => 0);
return { repo, stars };
});
```
> On edge workers the instance is destroyed after each request. Pass `event` as the **first** argument to the cached function (and to `getKey`) so Nitro can use `event.waitUntil` for background revalidation; on platforms with no request-carried `waitUntil`, pass the `waitUntil` option instead.
Cached values are JSON-serialized — don't return Symbols, Maps, or Sets (use `serialize`/`transform` for special shapes). Byte values (`Uint8Array`/`ArrayBuffer`) are handled and always returned as `Uint8Array`.
## Caching via route rules
Wrap handlers in caching by glob pattern without touching handler code:
```ts [nitro.config.ts]
import { defineConfig } from "nitro";
export default defineConfig({
kv: { redis: { driver: "redis", url: "redis://localhost:6379" } },
routeRules: {
"/blog/**": { swr: 3600 }, // SWR, 1h (swr:true = 1s default)
"/heavy/**": { cache: { maxAge: 3600, base: "redis" } }, // custom mountpoint
"/api/realtime/**": { cache: false }, // disable
},
});
```
Route-rule handlers use the group `nitro/route-rules`, and the generated `name` scopes the entry to the route, method and pattern (unique per process — set an explicit `name` to share entries across workers/restarts with persistent storage).
## Options
### Shared (`defineCachedHandler` + `defineCachedFunction`)
| Option | Default | Description |
|---|---|---|
| `maxAge` | `1` | Seconds the cache is valid. `0` disables caching. |
| `swr` | `false` | Serve stale while revalidating in background. |
| `staleMaxAge` | — | Extra seconds a stale value is served (only with `swr`). `0` = never stale; unset = no TTL written. |
| `base` | `/cache` | Storage base (→ `cache:` prefix). An array enables multi-tier reads/writes. |
| `name` | inferred | Cache namespace (falls back to a hash of the function source). |
| `group` | `nitro/handlers` / `nitro/functions` | Key group. |
| `getKey(...args)` | hash | Compute cache key (does not change request narrowing). |
| `integrity` | code+options hash | Invalidate when changed. |
| `getMaxAge(entry)` | — | Derive lifetime per entry from the resolved value. |
| `maxResolveTime` | `30` | Deadline (s) for one shared resolution. |
| `storage` | cache storage | Override the backend for this handler/function. |
| `waitUntil(promise)` | — | Hand background work to the host runtime. |
| `shouldInvalidateCache` / `shouldBypassCache` | — | Per-call predicates. |
| `onError(err)` | log | Custom error handling. |
### Handler-only
`varies` (header names in the key / kept on request), `allowQuery` (`boolean \| string[]`), `allowCookies` (`string[]`, emits `Vary: Cookie`), `allowAuthorization`, `sendCacheControl` (default `true`; `false` = server-only caching), `cacheStatusHeader` (default `X-Cache`), `stream`, `maxBodySize`, `headersOnly`, `shouldCache(entry)`.
### Function-only
`transform(entry, ...args)` (read-side, `entry.status` = `hit`/`stale`/`revalidated`/`miss`), `serialize(entry, ctx)` (write-side), `validate(entry, ctx)`.
## What gets cached
Only responses with status `200`/`203`/`301`/`308`, a body, no `cache-control` opt-out (`no-store`/`private`/`no-cache`/zero shared lifetime), and no `Vary: *` (or `Vary` naming a header outside the key) are stored. Failing responses are still returned, just not stored — and get no synthesized `cache-control`. `must-revalidate` only disables SWR for that response.
## Cache keys & invalidation
Key pattern: `` `${base}:${group}:${name}:${getKey(...args)}.json` `` (`group`/`name` are sanitized; a hash is appended if characters were removed).
Cached **functions** expose on-demand methods:
```ts
await cachedGHStars("unjs/nitro"); // populate
await cachedGHStars.expire("unjs/nitro"); // mark stale (SWR: served while refreshing)
await cachedGHStars.invalidate("unjs/nitro"); // remove entirely
await cachedGHStars.resolveKeys("unjs/nitro"); // storage key(s) for these args
```
> `defineCachedHandler` returns a plain handler and does **not** expose these methods — move expensive work into a `defineCachedFunction` (called from a plain `defineHandler`) and invalidate the function when you need on-demand invalidation.
## Cache storage
Entries live under the `cache:` prefix. By default they sit in the in-memory root storage (**not persisted**). Configure a persistent backend by mounting a `cache` point via the `kv` option (see [core-storage](core-storage.md)):
```ts [nitro.config.ts]
import { defineConfig } from "nitro";
export default defineConfig({
kv: { cache: { driver: "redis" /* options */ } },
$development: { kv: { cache: { driver: "fs", base: "./.data/cache" } } },
});
```
## Key Points
- Import from `nitro/cache`; `swr` and `maxAge` both default small — set them explicitly.
- `defineCachedFunction` caches reusable logic (and supports `.invalidate()`/`.expire()`); `defineCachedHandler` caches whole responses.
- On edge, always thread `event` through cached functions for `waitUntil`-backed refresh.
- Persist cache by mounting a `cache` point via the `kv` config option (not the removed `storage`).
<!--
Source references:
- https://nitro.build/docs/cache
- https://nitro.build/docs/routing#route-rules
- https://ocache.unjs.io
-->
references/core-configuration.md
---
name: configuration
description: nitro.config.ts, defineConfig, key build options, and runtime config in Nitro
---
# Configuration & Runtime Config
Nitro is configured via `nitro.config.ts` (loaded with [c12](https://github.com/unjs/c12)) or, when building with Vite, via the `nitro` key in `vite.config.ts`.
## Config file
```ts [nitro.config.ts]
import { defineConfig } from "nitro";
export default defineConfig({
// Nitro options
});
```
```ts [vite.config.ts]
import { defineConfig } from "vite";
import { nitro } from "nitro/vite";
export default defineConfig({
plugins: [nitro()],
nitro: {
// Nitro options
},
});
```
> v3 renamed the package `nitropack` → `nitro` and `defineNitroConfig` → `defineConfig`.
### Environment overrides & extending
```ts [nitro.config.ts]
export default defineConfig({
logLevel: 3,
$development: { debug: true }, // only during `nitro dev`
$production: { minify: true }, // only during `nitro build`
extends: "./base.config", // inherit from another config/preset
});
```
Config can also live under `.config/nitro.ts` or in a `.nitrorc` (`key=value`) file.
> **v3:** auto-imports are removed and the `imports` option is gone — add explicit imports everywhere (`nitro`, `nitro/kv`, `nitro/cache`, `nitro/runtime-config`, ...). Directory scanning is opt-in via `serverDir` (see below).
## Server directory scanning is opt-in
`serverDir` defaults to **`false`** — no `routes/`, `api/`, `middleware/`, `plugins/`, `tasks/`, etc. are scanned until you set it. `srcDir` is deprecated.
```ts [nitro.config.ts]
export default defineConfig({
serverDir: "./server", // or "." for the v2 root layout; true = "server"
});
```
## Key options
| Option | Purpose |
|---|---|
| `preset` / `defaultPreset` | Deployment target (or `NITRO_PRESET` / `--preset`); `defaultPreset` is the fallback. |
| `compatibilityDate` | Lock preset runtime behavior to a `YYYY-MM-DD` date. |
| `runtimeConfig` | Runtime values overridable via `NITRO_*` env vars. |
| `kv` | unstorage mounts (v2 `storage`). Override in dev/prerender via `$development.kv` / `$prerender.kv`. |
| `database` / `devDatabase` | DB connections (requires `experimental.database`). |
| `routeRules` | Per-route caching, headers, redirects, proxy, CORS. |
| `serverDir` | Scan dir (default `false`). `"./"` or `"./server"`; `scanDirs` adds more. |
| `serverEntry` / `renderer` | Catch-all fetch handler / catch-all renderer. |
| `errorHandler` / `devErrorHandler` | Custom error handler path(s) / dev-only handler fn. |
| `features` | Built-in features (`websocket`, `runtimeHooks`). |
| `experimental` | Opt-in features (`tasks`, `database`, `openAPI`, `asyncContext`, `envExpansion`). |
| `prerender` | `{ routes, crawlLinks, failOnError, concurrency, autoSubfolderIndex }`. |
| `minify`, `sourcemap`, `inlineDynamicImports` | Build output tuning. |
| `builder` | `"rollup"` \| `"rolldown"` \| `"vite"` (auto-detected; or `NITRO_BUILDER`). |
| `output` | `{ dir, serverDir, publicDir }` (defaults under `.output/`). |
### Directory defaults
| Option | Default |
|---|---|
| `rootDir` | `.` |
| `serverDir` | `false` |
| `buildDir` | `node_modules/.nitro` |
| `output.dir` | `.output` |
| `apiDir` / `routesDir` | `api` / `routes` |
## Runtime config
Define defaults in config; override at runtime with `NITRO_`-prefixed env vars. Only keys declared in `runtimeConfig` can be overridden — env vars cannot introduce new keys.
```ts [nitro.config.ts]
import { defineConfig } from "nitro";
export default defineConfig({
runtimeConfig: {
apiToken: "dev_token",
database: { host: "localhost", port: 5432 },
},
});
```
Access it (note the dedicated subpath import):
```ts [routes/example.get.ts]
import { defineHandler } from "nitro";
import { useRuntimeConfig } from "nitro/runtime-config";
export default defineHandler((event) => {
return useRuntimeConfig().apiToken;
});
```
Env var mapping uses `NITRO_` + `UPPER_SNAKE_CASE`, nested keys joined by `_`:
```bash [.env]
NITRO_API_TOKEN="123"
NITRO_DATABASE_HOST="db.example.com"
NITRO_DATABASE_PORT="5433"
```
- `.env` / `.env.local` are loaded when Nitro resolves config (both `nitro dev` and `nitro build`), so `nitro.config.ts` can read them; they are **not** loaded by the built server at runtime — in production use the platform's env vars.
- Values must be serializable; `undefined`/`null` fall back to `""`.
- Add a secondary prefix via `runtimeConfig.nitro.envPrefix: "APP_"` (checked alongside `NITRO_`). With no custom prefix, the default secondary prefix is `_` (e.g. `_API_TOKEN` also overrides `apiToken`).
- Enable `experimental.envExpansion` to expand `{{VAR}}` references inside runtime config strings.
## Environment variables (built-in)
| Variable | Effect |
|---|---|
| `NITRO_PRESET` | Override deployment preset. |
| `NITRO_COMPATIBILITY_DATE` | Set compatibility date. |
| `NITRO_APP_BASE_URL` | Override base URL (default `/`). |
| `NITRO_BUILDER` | Select the bundler (`rollup` / `rolldown` / `vite`). |
| `NITRO_ENV_PREFIX` | Secondary prefix for runtime-config overrides (default `_`). |
| `NITRO_ENV_EXPANSION` | Enable env expansion in runtime config values. |
## Key Points
- `defineConfig` from `nitro` is the v3 entry point; with Vite, options go under the `nitro` key.
- Auto-imports are removed — import every utility explicitly. Set `serverDir` or nothing is scanned.
- Use `useRuntimeConfig()` from `nitro/runtime-config` for env-overridable values; never read `process.env` in module top-level (edge runtimes only expose env during requests).
- `experimental.*` flags gate `tasks`, `database`, and `openAPI`.
- Types are imported from `nitro/types` (e.g. `NitroRuntimeConfig`), not `nitro`.
<!--
Source references:
- https://nitro.build/docs/configuration
- https://nitro.build/config
-->
references/core-rendering.md
---
name: rendering
description: Renderer for catch-all HTML/SSR, server entry global handler, and framework integration in Nitro
---
# Renderer & Server Entry
Two special handlers control requests that aren't matched by `routes/`: the **server entry** (runs first, for cross-cutting concerns) and the **renderer** (catch-all `/**`, lowest priority).
Matching order: `request` hook → route rules → middleware → specific routes → **server entry** → **renderer**.
## Server entry
Auto-detected from `server.ts` (or `.js/.mjs/.mts/.tsx/.jsx`) in `serverDir` (if set) or the project root. It is registered as a catch-all (`/**`) that runs only for requests **no route matched**, right before the renderer — it is a fallback, not global middleware (use [middleware](core-routing.md#middleware) for cross-cutting concerns). Its top-level code runs once at startup (before plugins). Return a `Response` to terminate, return nothing to continue.
```ts [server.ts]
export default {
async fetch(req: Request) {
const url = new URL(req.url);
if (url.pathname === "/health") {
return new Response("OK", { status: 200 });
}
// return nothing -> continue to routes/renderer
},
};
```
Or use `defineHandler` for the H3 event + context:
```ts [server.ts]
import { defineHandler } from "nitro";
export default defineHandler((event) => {
event.context.requestId = crypto.randomUUID();
// no return -> continue
});
```
Configure explicitly with `serverEntry` (a string, or `{ handler, format }`, or `false` to disable):
```ts [nitro.config.ts]
export default defineConfig({
serverEntry: { handler: "./server.ts", format: "web" }, // "web" (default) or "node"
});
```
### Typed server options
When the default export is a plain object, use `defineServerEntry` for typed [srvx](https://srvx.h3.dev/) server options (`port`, `hostname`, `tls`, `maxRequestBodySize`, ...), applied by the Node/Bun/Deno presets:
```ts [server.ts]
import { defineServerEntry } from "nitro";
export default defineServerEntry({
fetch: (req) => new Response("Hello"),
port: 8080,
maxRequestBodySize: 1024 * 1024,
});
```
`NITRO_PORT`/`PORT`, `NITRO_HOST`/`HOST`, `NITRO_SSL_CERT`/`NITRO_SSL_KEY` override the matching options. Options are read only from plain-object exports (not framework instances or `server.node.ts`).
### Framework integration
Any framework exposing a web `fetch(request): Response` works as a server entry:
```ts [server.ts]
import { Hono } from "hono";
const app = new Hono();
app.get("/", (c) => c.text("Hello from Hono!"));
export default app;
```
For Node-style `(req, res)` frameworks (Express, Fastify), name the file `server.node.ts` (or set `format: "node"`) — Nitro converts it via [srvx](https://srvx.h3.dev/):
```ts [server.node.ts]
import Express from "express";
const app = Express();
app.use("/", (_req, res) => res.send("Hello from Express!"));
export default app;
```
## Renderer
A catch-all that serves HTML/SSR for unmatched routes. Configured via `renderer`, or auto-detected from `index.html`.
```ts [nitro.config.ts]
export default defineConfig({
renderer: {
template: "./index.html", // HTML template
handler: "./renderer.ts", // OR a custom handler (template ignored if set)
static: false, // serve template as-is, skip processing
},
});
```
Set `renderer: false` to disable entirely.
### Auto-detected `index.html` (SPA)
If an `index.html` exists, Nitro serves it for all unmatched routes — the default SPA behavior with Vite. With a Vite `ssr` environment, add `<!--ssr-outlet-->` and Nitro injects SSR output.
### Custom renderer handler
```ts [renderer.ts]
export default function renderer({ req }: { req: Request }) {
const url = new URL(req.url);
return new Response(
`<!DOCTYPE html><html><body><h1>${url.pathname}</h1></body></html>`,
{ headers: { "content-type": "text/html; charset=utf-8" } },
);
}
```
### Rendu templates (experimental)
HTML templates support the [rendu](https://github.com/h3js/rendu) preprocessor:
```html [index.html]
<h1>Hello {{ $URL.pathname }}</h1>
<? if ($METHOD === "POST") { ?><p>Submitted!</p><? } ?>
<script server>
const data = await fetch("https://api.example.com/data").then((r) => r.json());
</script>
<pre>{{ JSON.stringify(data) }}</pre>
```
- `{{ expr }}` HTML-escaped, `{{{ expr }}}` / `<?= expr ?>` raw, `<? ... ?>` control flow.
- Globals: `$REQUEST`, `$METHOD`, `$URL`, `$HEADERS`, `$RESPONSE`, `$COOKIES`.
- Functions: `setCookie`, `redirect`, `echo` (streaming), `htmlspecialchars`.
## Key Points
- Server entry = global pre-routing handler (auth, logging); renderer = catch-all HTML/SSR.
- Server entry returns `Response` to stop, or nothing to continue; keep it lightweight (runs every request).
- Web-fetch frameworks plug into `server.ts`; Node `(req,res)` ones use `server.node.ts`.
- A defined `[...].ts` catch-all route conflicts with the renderer (Nitro warns).
<!--
Source references:
- https://nitro.build/docs/server-entry
- https://nitro.build/docs/renderer
- https://nitro.build/docs/lifecycle
-->
references/core-routing.md
---
name: routing
description: File-based routing, event handlers, dynamic params, middleware, and route rules in Nitro
---
# Routing & Event Handlers
Nitro maps files in `routes/` and `api/` to HTTP routes at build time (no runtime router). Handlers receive an [H3 v2](https://h3.dev) `event` and should **return** the response body or **throw** an error.
> **v3:** filesystem routing only runs when [`serverDir`](core-configuration.md) (or `scanDirs`) is set — no directories are scanned by default. Auto-imports are removed, so import `defineHandler`/`HTTPError` explicitly.
## Event handlers
```ts [routes/hello.ts]
import { defineHandler } from "nitro";
export default defineHandler((event) => {
return { hello: "world" };
});
```
`defineHandler` gives type inference. A plain `(event) => ...` function also works. The `event` is web-standard based:
```ts
event.req // web Request
event.res // response init (headers, status)
event.url // URL object (event.url.pathname, event.url.searchParams)
event.path // request path
event.method // HTTP method
event.context // mutable per-request context (params, custom data)
event.context.params // route params
```
Read the body with native `Request` methods (H3 v2 dropped `readBody`):
```ts
const json = await event.req.json();
const text = await event.req.text();
const form = await event.req.formData();
```
## Filesystem routing
Files in `api/` (served under `/api`) or `routes/` (served under `/`) become routes. One handler per file.
```
routes/
hello.ts -> /hello
api/
test.ts -> /api/test
[org]/
[repo]/
index.ts -> /api/:org/:repo
issues.ts -> /api/:org/:repo/issues
```
### HTTP method suffix
Append the method to match only that verb (`get`, `post`, `put`, `delete`, `patch`, `head`, `options`, `query`, `connect`, `trace`):
```ts [routes/users.post.ts]
import { defineHandler } from "nitro";
export default defineHandler(async (event) => {
const body = await event.req.json();
return { created: body };
});
```
### Dynamic params
```ts [routes/hello/[name].ts]
export default defineHandler((event) => {
const { name } = event.context.params!;
return `Hello ${name}!`;
});
```
- Multiple params: each as its own folder/segment `[a]/[b]` (not in one filename).
- Catch-all: `[...name].ts` captures the rest of the path (includes `/`).
- Global catch-all: `[...].ts` matches all otherwise-unmatched routes; its wildcard is on `event.context.params._`. It chains before the [server entry](core-rendering.md) and renderer.
### Route groups & environment handlers
- Parenthesized folders `(admin)/` group files **without** affecting the URL.
- Suffix `.dev`, `.prod`, or `.prerender` (after the method suffix) to include a handler only in that build: `test.get.prod.ts`.
- `ignore: ["routes/**/_*"]` config excludes files from scanning.
## Middleware
Files in `middleware/` run on every request before route matching. They modify the event and **must not return** (returning ends the request).
```ts [middleware/auth.ts]
import { defineHandler } from "nitro";
export default defineHandler((event) => {
event.context.user = { name: "Nitro" };
});
```
Control execution order with numeric prefixes (`1.logger.ts`, `2.auth.ts` — pad to `01.` etc. beyond 9 to keep string sort correct). Scope manually with `event.url.pathname`, or register route-scoped middleware in config (keep the file **outside** `middleware/` so it isn't also registered globally):
```ts [nitro.config.ts]
export default defineConfig({
handlers: [
{ route: "/api/**", handler: "./utils/api-auth.ts", middleware: true },
],
});
```
## Programmatic routes
Register handlers/middleware in config in addition to (or instead of) the filesystem:
```ts [nitro.config.ts]
export default defineConfig({
routes: {
"/api/hello": "./routes/api/hello.ts",
"/api/custom": { handler: "./routes/custom.ts", method: "POST", lazy: true },
},
handlers: [
{ route: "/blog/**", handler: "./handlers/blog.ts", method: "get" },
],
});
```
Handler options: `handler`, `method`, `lazy`, `middleware`, `format` (`"web"` | `"node"`), `env`.
## Error handling
Throw `HTTPError` (replaces v2 `createError`):
```ts
import { defineHandler, HTTPError } from "nitro";
export default defineHandler((event) => {
const user = findUser(event.context.params!.id);
if (!user) {
throw new HTTPError({ status: 404, message: "User not found" });
}
return user;
});
```
`HTTPError` has several forms: `new HTTPError("msg", { status: 400 })`, `HTTPError.status(400, "Bad Request")`, or the full object form. Any other thrown value is treated as unhandled (always `500`, message/stack hidden).
In dev, browsers (Accept: text/html) get an HTML error page; production always returns JSON (`{ error, status, message, data }`). Customize with the `errorHandler` config (a path or array of paths) to a module exporting `defineErrorHandler((error, event) => Response)` from `nitro`. Handlers run in order (first response wins; built-in default is always appended); `devErrorHandler` overrides dev-only rendering.
## Route rules
Apply per-route behavior (caching, headers, redirects, proxy, CORS) by glob pattern (rou3 syntax). Rules merge least-specific to most-specific; set a rule to `false` to disable an inherited one. When `cache` is set, matching handlers are auto-wrapped with `defineCachedHandler`.
```ts [nitro.config.ts]
import { defineConfig } from "nitro";
export default defineConfig({
routeRules: {
"/blog/**": { swr: 600 }, // stale-while-revalidate w/ maxAge
"/api/data/**": { cache: { maxAge: 60 } }, // full cache options
"/api/realtime/**": { cache: false }, // disable caching
"/assets/**": { headers: { "cache-control": "s-maxage=0" } },
"/api/public/**": { cors: true }, // permissive CORS defaults
"/api/v1/**": { cors: { origin: ["https://app.example.com"], credentials: true } },
"/old-page": { redirect: "/new-page" }, // 307 by default
"/legacy": { redirect: { to: "https://example.com/", status: 308 } },
"/old-blog/**": { redirect: "https://blog.example.com/**" }, // wildcard preserves suffix
"/proxy/**": { proxy: "https://api.example.com/**" },
"/about": { prerender: true },
"/isr/**": { isr: 60 }, // Vercel ISR
},
});
```
Route rule keys: `headers`, `redirect`, `proxy`, `cors`, `cache`, `swr`, `static`, `prerender`, `isr`. `swr: true` = `cache: { swr: true }` (1s default `maxAge`); `swr: <n>` adds `maxAge: <n>`. Rules can also be supplied via `runtimeConfig.nitro.routeRules` for env-var overrides without rebuilding.
> **No `auth`/`basicAuth` route rule.** Auth needs executable logic → use [middleware](#middleware). For a single route use h3's `basicAuth` from `nitro/h3` in the handler's `middleware` array:
> ```ts
> import { defineHandler } from "nitro";
> import { basicAuth } from "nitro/h3";
> export default defineHandler({
> middleware: [basicAuth({ username: "admin", password: "secret" })],
> handler: (event) => `Hello, ${event.context.basicAuth?.username}!`,
> });
> ```
### Method-scoped rules
Prefix a key with an uppercase HTTP method + space to scope it; unprefixed keys apply to every method and are merged underneath:
```ts
routeRules: {
"/api/**": { headers: { "x-api": "true" } }, // every method
"POST /api/**": { headers: { "x-write": "true" } },
"GET /feed": { swr: 600 },
}
```
> Platform-native static config (Netlify/Cloudflare `_headers`/`_redirects`, Vercel `config.json`) does not split by method — prefer method-agnostic keys for `headers`/`redirect`/`proxy` you expect emitted statically. Do not combine `prerender` and `isr` on the same route.
## Key Points
- Handlers **return** the body or **throw**; H3 v1 `send*` helpers are gone.
- Use `event.req.json()/text()/formData()` instead of v2 `readBody`.
- Params live on `event.context.params` (unnamed catch-all on `params._`).
- Each route handler is a separate code-split chunk (set `inlineDynamicImports: true` to bundle into one file).
- Route rules wrap matching handlers in caching, proxying, redirects, and CORS without handler code — auth goes in middleware.
<!--
Source references:
- https://nitro.build/docs/routing
- https://nitro.build/docs/lifecycle
- https://nitro.build/config
-->
references/core-storage.md
---
name: storage
description: Runtime-agnostic KV storage via unstorage, useKV, mount points, and drivers in Nitro
---
# KV Storage
Nitro ships a runtime-agnostic key-value layer powered by [unstorage](https://unstorage.unjs.io). Access it with `useKV()` from `nitro/kv`.
> **v3 rename:** `useStorage` → `useKV` (imported from `nitro/kv`, not `nitro/storage`), and the `storage` config option → `kv`. The old names still work as deprecated aliases. `devStorage` is replaced by `kv` inside `$development` (and `$prerender`).
## Usage
```ts
import { useKV } from "nitro/kv";
// Default storage (in-memory, not persisted across restarts)
await useKV().setItem("test:foo", { hello: "world" });
const value = await useKV().getItem("test:foo");
// Scope to a base/mountpoint
const test = useKV("test");
await test.setItem("foo", { hello: "world" });
// Type the return value
await useKV<{ hello: string }>("test").getItem("foo");
await useKV("test").getItem<{ hello: string }>("foo");
```
### Common methods
| Method | Description |
|---|---|
| `getItem(key)` / `setItem(key, val)` | Read / write (returns `null` if missing). |
| `getItemRaw` / `setItemRaw` | Binary / unserialized values. |
| `getItems` / `setItems` | Batch operations. |
| `hasItem(key)` | Existence check. |
| `removeItem(key)` | Delete a key. |
| `getKeys(base?)` / `clear(base?)` | List / clear by prefix. |
| `getMeta(key)` / `setMeta(key, meta)` / `removeMeta(key)` | Metadata (mtime, atime, etag, type, ttl). |
| `mount(base, driver)` / `unmount(base)` | Dynamically attach a driver. |
| `watch(cb)` / `unwatch()` | React to `"update"` / `"remove"` events. |
Aliases: `get`, `set`, `has`, `del`, `remove`, `keys`.
## Configuring drivers
Mount drivers by name via the `kv` option; the key is the mount point, the value is the driver config.
```ts [nitro.config.ts]
import { defineConfig } from "nitro";
export default defineConfig({
kv: {
redis: { driver: "redis", url: "redis://localhost:6379" },
},
// Override mounts in dev (e.g. when the prod driver isn't available locally)
$development: {
kv: {
redis: { driver: "fs", base: "./.data/redis" },
},
},
});
```
Then `useKV("redis")`. See the [unstorage drivers list](https://unstorage.unjs.io) (fs, memory, redis, cloudflare-kv, s3, ...).
- `$development.kv` mounts are merged on top of `kv`. A mount with a **different** `driver` replaces the whole mount; the **same** `driver` deep-merges options.
- Use `$prerender.kv` to override mounts during prerendering only.
- Mount options are serialized into the build output, so they must be JSON-serializable — use runtime mounts (below) for non-serializable options.
- Driver third-party libs (e.g. `ioredis` for `redis`) are auto-detected and passed via the driver's `lib` option so bundlers can resolve them; `lib` is therefore not configurable in `kv` mounts (set `null` to opt out).
## Built-in mount points
- **Default** (no base): in-memory, not persisted. Mount an `fs`/`redis` driver to persist.
- **`assets:server`** (`/assets` base): read-only access to bundled server assets — see [core-assets](core-assets.md).
- **`cache:`**: used by the caching layer; lives in root storage unless you mount a dedicated `cache` point — see [core-cache](core-cache.md).
## Runtime / dynamic mounts
When credentials are only known at runtime, mount a driver from a [plugin](features-plugins.md):
```ts [plugins/storage.ts]
import { definePlugin } from "nitro";
import { useKV } from "nitro/kv";
import redisDriver from "unstorage/drivers/redis";
export default definePlugin(() => {
useKV().mount("redis", redisDriver({
base: "redis",
host: process.env.REDIS_HOST,
port: Number(process.env.REDIS_PORT),
lib: () => import("ioredis"), // pass the lib explicitly so it is bundled
}));
});
```
> Runtime mounts bypass Nitro's driver-dependency detection: install the third-party lib yourself and pass it via the driver's `lib` option, or the mount fails at runtime.
## Key Points
- Import `useKV` from `nitro/kv` (v2 `useStorage` / `nitro/storage` are deprecated aliases). No auto-imports in v3.
- Default storage is in-memory; configure a persistent driver via `kv` for production.
- Use `$development.kv` (not the removed `devStorage`) to swap drivers in development.
- Prefer this abstraction over platform-specific KV APIs (e.g. Cloudflare KV) for portability.
<!--
Source references:
- https://nitro.build/docs/storage
- https://unstorage.unjs.io
-->
references/deploy-presets.md
---
name: deploy-presets
description: Deployment presets for runtimes and providers, compatibility dates, and platform integration in Nitro
---
# Deployment Presets
Nitro builds the same code into different output formats via **presets**. Most providers are auto-detected in CI; otherwise set the preset explicitly.
```bash
nitro build --preset cloudflare_module
# or env var:
NITRO_PRESET=vercel nitro build
```
```ts [nitro.config.ts]
import { defineConfig } from "nitro";
export default defineConfig({
preset: "cloudflare_pages",
defaultPreset: "node_cluster", // fallback when nothing is detected
});
```
The default production preset is `node_server`; dev always uses the isolated `nitro_dev` preset.
## Runtime presets
| Preset | Runtime / Notes |
|---|---|
| `node_server` | Default. `node .output/server/index.mjs` starts a ready server. |
| `node_cluster` | Multi-core via `node:cluster` (`NITRO_CLUSTER_WORKERS`). |
| `node_middleware` | Exports a `(req, res)` middleware/`listener` for custom servers. |
| `bun` | Optimized [Bun](https://bun.sh) output: `bun run ./.output/server/index.mjs`. |
| `deno_server` | [Deno](https://deno.com) server output. |
Node server env vars: `NITRO_PORT`/`PORT` (3000), `NITRO_HOST`/`HOST`, `NITRO_UNIX_SOCKET`, `NITRO_SSL_CERT`/`NITRO_SSL_KEY`, and graceful-shutdown controls (`NITRO_SHUTDOWN_*`).
## Provider presets
| Provider | Preset(s) | Auto-detect |
|---|---|---|
| Cloudflare | `cloudflare_module` (recommended), `cloudflare_pages` | ✅ |
| Vercel | `vercel` (Fluid compute) | ✅ |
| Netlify | `netlify`, `netlify_edge` | ✅ |
| AWS | `aws_amplify`, `aws_lambda` | Amplify ✅ |
| Azure | `azure_swa` | ✅ |
| Deno Deploy | `deno_deploy` | — |
| Firebase | `firebase_app_hosting` | ✅ |
| Others | GitHub/GitLab Pages, DigitalOcean, Heroku, Render, Zeabur, Stormkit, Koyeb, ... | varies |
## Compatibility dates
Providers evolve; Nitro uses a compatibility date to lock behavior at project creation and opt into updates deliberately.
```ts [nitro.config.ts]
export default defineConfig({
compatibilityDate: "2025-01-01",
});
```
## Platform integration highlights
**Cloudflare** (`cloudflare_module`): access bindings via `event.req.runtime.cloudflare.env`:
```ts
import { defineHandler } from "nitro";
export default defineHandler(async (event) => {
const { env } = event.req.runtime.cloudflare;
const { results } = await env.MY_D1.prepare("SELECT id FROM t").all();
return results;
});
```
Dev uses Miniflare to emulate bindings (define them in `wrangler.json`/`.toml` or inline via `cloudflare.wrangler`). `scheduledTasks` auto-generate Cron Triggers. Platform hooks: `cloudflare:scheduled`, `cloudflare:email`, `cloudflare:queue`, `cloudflare:tail`.
**Vercel** (`vercel`): use `routes/api/` (not `api/`). `scheduledTasks` → Vercel Cron Jobs (secure with `CRON_SECRET`); `isr` route rules → ISR; external `proxy` route rules become CDN rewrites. Per-route function config via `vercel.functionRules`; Bun runtime via `vercel.functions.runtime`. Vercel Queues via the `vercel:queue` hook.
> Prefer Nitro's portable abstractions ([storage](core-storage.md), [database](features-database.md)) over low-level platform bindings for stability across targets.
## Key Points
- One codebase → many targets; switch with `preset` / `NITRO_PRESET` / `--preset`.
- `node_server` is the default; `bun` and `deno_server` optimize for those runtimes.
- Cloudflare/Vercel/Netlify are zero-config in their CI; set the preset explicitly elsewhere.
- Set `compatibilityDate` and update it deliberately (test after each bump).
- v2→v3 renamed many presets (e.g. `cloudflare*` → `cloudflare_module`, `vercel-edge` → `vercel`, `azure*` → `azure_swa`).
<!--
Source references:
- https://nitro.build/deploy
- https://nitro.build/deploy/runtimes/node
- https://nitro.build/deploy/providers/cloudflare
- https://nitro.build/deploy/providers/vercel
-->
references/features-database.md
---
name: database
description: Built-in SQL database layer via db0, useDatabase, connectors, and dev overrides in Nitro
---
# Database
Nitro ships a lightweight SQL layer powered by [db0](https://db0.unjs.io). It defaults to SQLite (`.data/db.sqlite`) and works out of the box in dev and Node.js production. **Experimental** — enable the flag.
```ts [nitro.config.ts]
import { defineConfig } from "nitro";
export default defineConfig({
experimental: { database: true },
});
```
## Usage
Import `useDatabase()` from `nitro/database` (no auto-imports in v3). It returns a connection; the optional name defaults to `"default"`.
```ts [routes/users.ts]
import { defineHandler } from "nitro";
import { useDatabase } from "nitro/database";
export default defineHandler(async () => {
const db = useDatabase();
await db.sql`CREATE TABLE IF NOT EXISTS users (
"id" TEXT PRIMARY KEY, "firstName" TEXT, "lastName" TEXT, "email" TEXT
)`;
const id = String(Math.round(Math.random() * 10_000));
await db.sql`INSERT INTO users VALUES (${id}, 'John', 'Doe', '')`;
const { rows } = await db.sql`SELECT * FROM users WHERE id = ${id}`;
return { rows };
});
```
Connections are created lazily and cached per name.
### Query APIs
```ts
// Tagged template with safe parameter binding
const { rows } = await db.sql`SELECT * FROM users WHERE id = ${id}`;
const res = await db.sql`INSERT INTO posts (title) VALUES (${"Hello"})`;
// res.rows, res.changes, res.lastInsertRowid
// Raw string execution
await db.exec("CREATE TABLE IF NOT EXISTS t (id TEXT)");
// Prepared statement
const stmt = db.prepare("SELECT * FROM users WHERE id = ?");
const result = await stmt.bind("1001").all();
```
> Always use `db.sql` tagged templates (or `prepare().bind()`) for user input — they parameterize and prevent SQL injection.
## Configuration
```ts [nitro.config.ts]
import { defineConfig } from "nitro";
export default defineConfig({
experimental: { database: true },
database: {
default: { connector: "sqlite", options: { name: "db" } },
users: {
connector: "postgresql",
options: { url: "postgresql://user:pass@host:5432/db" },
},
},
// Use a local SQLite db in development while prod uses Postgres
devDatabase: {
default: { connector: "sqlite", options: { name: "dev-db" } },
},
});
```
Use a named connection with `useDatabase("users")`.
## Connectors
All [db0 connectors](https://db0.unjs.io/connectors) are supported, including: `sqlite` / `node-sqlite`, `better-sqlite3`, `sqlite3`, `bun` / `bun-sqlite`, `libsql` (+ `libsql-http`/`libsql-web`/`libsql-core`), `postgresql`, `mysql2`, `pglite`, `planetscale`, `neon`, `cloudflare-d1`, and Cloudflare Hyperdrive variants. Connector-specific settings (`url`, `host`, `name`, ...) go under `options`, not the top level. Third-party libs (e.g. `pg` for `postgresql`) are auto-detected/installed and passed via the connector's `lib` option.
## Key Points
- Requires `experimental.database: true`; defaults to a zero-config SQLite connection.
- `useDatabase()` from `nitro/database` (explicit import — no auto-imports in v3); names default to `"default"`.
- Prefer `db.sql` tagged templates for safe, parameterized queries.
- `devDatabase` **replaces** `database` entirely in dev (not merged per-name) — include every connection you need.
- Integrates with db0-supported ORMs; prefer this layer over platform-specific DB bindings for portability.
<!--
Source references:
- https://nitro.build/docs/database
- https://db0.unjs.io
-->
references/features-openapi.md
---
name: openapi
description: Auto-generated OpenAPI spec, defineRouteMeta, Scalar/Swagger UIs, and production exposure in Nitro
---
# OpenAPI
Nitro scans route handlers, reads metadata from `defineRouteMeta`, and generates an OpenAPI 3.1.0 spec plus interactive UIs ([Scalar](https://scalar.com/) and Swagger UI). **Experimental** — enable the flag.
```ts [nitro.config.ts]
import { defineConfig } from "nitro";
export default defineConfig({
experimental: { openAPI: true },
});
```
Dev endpoints once enabled:
| Endpoint | Description |
|---|---|
| `/_openapi.json` | OpenAPI 3.1.0 JSON spec |
| `/_scalar` | Scalar API reference UI |
| `/_swagger` | Swagger UI |
## Route metadata
`defineRouteMeta` is a build-time macro (no runtime overhead). Its `openAPI` field accepts a standard OpenAPI Operation Object.
```ts [routes/api/users/[id].get.ts]
import { defineRouteMeta, defineHandler } from "nitro";
defineRouteMeta({
openAPI: {
tags: ["users"],
description: "Get a user by ID",
parameters: [
{ in: "query", name: "include", schema: { type: "string" } },
],
responses: {
200: {
description: "User found",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/User" },
},
},
},
404: { description: "Not found" },
},
// Hoist reusable schemas into the top-level components section
$global: {
components: {
schemas: {
User: {
type: "object",
properties: {
id: { type: "string" },
name: { type: "string" },
email: { type: "string", format: "email" },
},
},
},
},
},
},
});
export default defineHandler((event) => {
const { id } = event.context.params!;
return { id, name: "Alice", email: "alice@example.com" };
});
```
- Route params (`[id]`/`:id`) become OpenAPI path parameters automatically — only declare extra query/header params.
- Auto-tagging by prefix: `/api/` → API Routes, `/_` → Internal, others → App Routes. Override with `tags`.
- Define a schema once under `$global.components.schemas`, reference it elsewhere with `$ref`.
## Configuration
```ts [nitro.config.ts]
import { defineConfig } from "nitro";
export default defineConfig({
experimental: { openAPI: true },
openAPI: {
meta: { title: "My API", description: "...", version: "2.0.0" },
route: "/_docs/openapi.json", // override JSON path
ui: {
scalar: { route: "/_docs/scalar", theme: "purple" },
swagger: false, // disable a UI
},
production: "runtime", // expose in production
},
});
```
`production` values:
| Value | Behavior |
|---|---|
| `false` (default) | Disabled in production. |
| `"runtime"` | Generated per request (allows middleware/dynamic info). |
| `"prerender"` | Generated at build time, served as a static file (most efficient). |
> If exposed in production, protect these endpoints with authentication.
## Key Points
- Requires `experimental.openAPI: true`; configure via the top-level `openAPI` option.
- `defineRouteMeta` (from `nitro`) is a build-time macro — zero runtime cost.
- Path params are inferred from the route; declare only additional parameters.
- OpenAPI is dev-only by default; opt into `"runtime"`/`"prerender"` for production and secure the routes.
<!--
Source references:
- https://nitro.build/docs/openapi
- https://nitro.build/config
-->
references/features-plugins.md
---
name: plugins
description: Nitro plugins, runtime lifecycle hooks, error capture, and request/response handling
---
# Plugins & Lifecycle Hooks
Nitro plugins run **once** at server startup to extend runtime behavior. They are auto-registered from `plugins/` (by filename order) and receive the `nitroApp` context. Import `definePlugin` from `nitro`.
```ts [plugins/setup.ts]
import { definePlugin } from "nitro";
export default definePlugin((nitroApp) => {
console.log("Nitro starting");
});
```
> v3 renamed `defineNitroPlugin` → `definePlugin`. The plugin function itself must be synchronous (return `void`); the hooks it registers may be async. Register external plugins with the `plugins: ["~/plugins/x.ts"]` config option.
## The `nitroApp` context
| Property | Description |
|---|---|
| `hooks` | [hookable](https://github.com/unjs/hookable) instance to register lifecycle callbacks. |
| `h3` | The underlying H3 app instance. |
| `fetch(req)` | The app's internal fetch handler. |
| `captureError(error, ctx)` | Feed errors into the error hook pipeline. |
## Runtime hooks
Register lifecycle callbacks inside a plugin. `hook()` returns an unregister function.
| Hook | Signature | When |
|---|---|---|
| `request` | `(event) => void \| Promise` | Start of each request, before routing. |
| `response` | `(res: Response, event) => void \| Promise` | After the response is created. |
| `error` | `(error, { event?, tags? }) => void` | When any error is captured. |
| `close` | `() => void` | On server shutdown. |
```ts [plugins/lifecycle.ts]
import { definePlugin } from "nitro";
export default definePlugin((nitroApp) => {
nitroApp.hooks.hook("request", (event) => {
console.log("→", event.path);
});
nitroApp.hooks.hook("response", (res, event) => {
const { pathname } = new URL(event.req.url);
if (pathname.endsWith(".js")) res.headers.append("Vary", "Origin");
});
nitroApp.hooks.hook("close", async () => {
// clean up DB connections, timers, etc.
});
});
```
The `request` hook is the very first code per request (errors there are captured, not fatal). The `response` hook runs for every response, including static assets and errors. On shutdown the server waits for pending `event.waitUntil` tasks.
## Error capture
```ts [plugins/errors.ts]
import { definePlugin } from "nitro";
export default definePlugin((nitroApp) => {
nitroApp.hooks.hook("error", (error, { event, tags }) => {
console.error(`[${tags?.join(",")}] ${event?.path}`, error);
});
// Manually report
nitroApp.captureError(new Error("startup issue"), { tags: ["startup"] });
});
```
`tags` identify the source: `"request"`, `"response"`, `"cache"`, `"plugin"`, `"unhandledRejection"`, `"uncaughtException"`. Process-level unhandled rejections and uncaught exceptions are auto-captured into the `error` hook.
## Notes
- `features.runtimeHooks` is auto-enabled when at least one plugin exists; force with `features.runtimeHooks: true`.
- `NitroRuntimeHooks` is augmentable — presets add platform hooks (e.g. Cloudflare `cloudflare:scheduled`, `cloudflare:email`; Vercel `vercel:queue`).
- `useNitroApp`, `useNitroHooks`, and `getRouteRules` are imported from `nitro/app` (no auto-imports in v3). Outside plugins, prefer `useNitroHooks()` over `useNitroApp().hooks` to guarantee a hooks instance.
- The `hook()` method returns an unregister function.
- For internal requests to your own server, use `serverFetch` from `nitro` (v2 `localFetch` is gone).
## Key Points
- Plugins run once at startup; use them to init resources and register hooks.
- Import `definePlugin` from `nitro` (not `defineNitroPlugin`); `useNitroApp`/`useNitroHooks` from `nitro/app`.
- Use `request`/`response`/`error`/`close` hooks for cross-cutting runtime behavior.
- Customize response headers in the `response` hook via `res.headers`.
<!--
Source references:
- https://nitro.build/docs/plugins
- https://nitro.build/docs/lifecycle
-->
references/features-tasks.md
---
name: tasks
description: On-demand and scheduled (cron) tasks in Nitro, defineTask, runTask, and platform support
---
# Tasks
Tasks are one-off runtime operations (migrations, cleanups, cache refresh). **Experimental** — enable the flag first. Tasks are scanned from `serverDir`, which must also be set.
```ts [nitro.config.ts]
import { defineConfig } from "nitro";
export default defineConfig({
serverDir: "./server",
experimental: { tasks: true },
});
```
## Defining tasks
Files in `<serverDir>/tasks/[name].ts`. Nested dirs join with `:` (e.g. `tasks/db/migrate.ts` → `db:migrate`). Import `defineTask` from `nitro/task` (no auto-imports in v3).
```ts [server/tasks/db/migrate.ts]
import { defineTask } from "nitro/task";
export default defineTask({
meta: {
name: "db:migrate",
description: "Run database migrations",
},
run({ payload, context }) {
console.log("Running DB migration...");
return { result: "Success" };
},
});
```
`run` receives a `TaskEvent` with `name`, `payload` (`Record<string, unknown>`), and `context` (may include `waitUntil`). Return `{ result }`.
Tasks can also be registered in config (config `handler` wins over a scanned file of the same name):
```ts [nitro.config.ts]
import { fileURLToPath } from "node:url";
export default defineConfig({
serverDir: "./server",
experimental: { tasks: true },
tasks: {
"db:migrate": { description: "Describe a scanned task" },
// A config handler is imported as-is (NOT resolved against rootDir) — use an absolute path:
"db:seed": {
handler: fileURLToPath(new URL("scripts/seed.ts", import.meta.url)),
description: "Seed the database",
},
},
});
```
## Scheduled tasks (cron)
Map cron expressions to task name(s). Multiple tasks under one expression run in parallel; scheduled runs get a `payload.scheduledTime` timestamp.
```ts [nitro.config.ts]
import { defineConfig } from "nitro";
export default defineConfig({
scheduledTasks: {
"* * * * *": ["cms:update"], // every minute
"0 0 * * *": "db:cleanup", // daily (string shorthand)
"*/5 * * * *": ["health:check", "metrics:collect"],
},
});
```
Platform support:
- `dev`, `node_server`, `node_cluster`, `node_middleware`, `bun`, `deno_server` → [croner](https://croner.56k.guru/) engine.
- `cloudflare_module` / `cloudflare_pages` → native Cron Triggers (wrangler config auto-generated).
- `vercel` → native Cron Jobs (config auto-generated; secure with `CRON_SECRET`).
## Running tasks programmatically
```ts [api/migrate.post.ts]
import { defineHandler } from "nitro";
import { runTask } from "nitro/task";
export default defineHandler(async (event) => {
// IMPORTANT: authenticate and validate before running!
const payload = Object.fromEntries(event.url.searchParams);
const { result } = await runTask("db:migrate", { payload });
return { result };
});
```
`runTask` throws a `404` if the task doesn't exist, `501` if it has no handler; errors from `run` propagate to the caller.
## Background work with `waitUntil`
```ts [tasks/sync.ts]
import { defineTask } from "nitro/task";
export default defineTask({
run({ context }) {
const promise = fetch("https://api.example.com/sync");
context.waitUntil?.(promise);
return promise.then(() => ({ result: "ok" }));
},
});
```
## Dev server tools
While `nitro dev` runs:
- `GET /_nitro/tasks` — list available tasks + scheduled tasks.
- `GET|POST /_nitro/tasks/:name` — execute (payload from query and/or JSON body under `"payload"`).
- CLI: `nitro task list` and `nitro task run db:migrate --payload "{}"`.
## Key Points
- Requires `experimental.tasks: true` **and** `serverDir` set; `defineTask` and `runTask` both come from `nitro/task` (no auto-imports in v3).
- Each task has one running instance — parallel calls of the same name share a single run/result.
- `scheduledTasks` cron config is translated to native triggers on Cloudflare and Vercel automatically.
<!--
Source references:
- https://nitro.build/docs/tasks
-->
references/features-websocket.md
---
name: websocket
description: Cross-platform WebSocket handlers, pub/sub, namespaces, and Server-Sent Events in Nitro
---
# WebSocket & SSE
Nitro provides cross-platform WebSockets via [CrossWS](https://crossws.h3.dev/) (Node, Bun, Deno, Cloudflare). Enable the feature first:
```ts [nitro.config.ts]
import { defineConfig } from "nitro";
export default defineConfig({
features: { websocket: true },
});
```
## Handlers
Export a `defineWebSocketHandler` (from `nitro`) in a route file — same file-based routing as HTTP handlers (`routes/_ws.ts` → `/_ws`), so `serverDir` must be set.
```ts [routes/_ws.ts]
import { defineWebSocketHandler } from "nitro";
export default defineWebSocketHandler({
open(peer) {
console.log("connected", peer.id);
peer.send("Welcome!");
},
message(peer, message) {
peer.send(`echo: ${message.text()}`);
},
close(peer, details) {
console.log("closed", details.code, details.reason);
},
error(peer, error) {
console.error(error);
},
});
```
### `upgrade` hook
Runs before the connection opens — authenticate, set namespace, attach context. Throw a `Response` to reject.
```ts [routes/chat.ts]
import { defineWebSocketHandler } from "nitro";
export default defineWebSocketHandler({
upgrade(request) {
const token = new URL(request.url).searchParams.get("token");
if (!isValid(token)) throw new Response("Unauthorized", { status: 401 });
return { context: { userId: getUserId(token) } }; // also: headers, protocol, namespace, handled
},
open(peer) {
console.log("user", peer.context.userId);
},
});
```
## Peer & message
`peer` (in all hooks except `upgrade`) exposes `id`, `namespace`, `context`, `request`, `peers`, `topics`, `remoteAddress`, `websocket`, plus methods:
```ts
peer.send("text"); // or an object -> JSON
peer.subscribe("topic");
peer.unsubscribe("topic");
peer.publish("topic", data); // broadcast to subscribers (not the sender)
peer.close(1000, "bye");
peer.terminate();
```
The `message` object: `message.text()`, `message.json<T>()`, `message.uint8Array()`, `message.arrayBuffer()`, `message.blob()`.
## Pub/Sub & namespaces
```ts [routes/rooms/[room].ts]
import { defineWebSocketHandler } from "nitro";
export default defineWebSocketHandler({
open(peer) {
peer.subscribe("messages");
peer.publish("messages", `${peer} joined ${peer.namespace}`);
},
message(peer, message) {
peer.publish("messages", `${peer}: ${message.text()}`); // only same namespace
},
close(peer) {
peer.publish("messages", `${peer} left`);
},
});
```
Namespaces isolate pub/sub groups. By default the namespace is the request pathname — so dynamic routes (`/rooms/lobby` vs `/rooms/game`) are isolated automatically. Override by returning `namespace` from `upgrade`.
## Server-Sent Events (SSE)
Simpler server→client streaming over HTTP (auto-reconnects). Use `createEventStream` from `nitro/h3`:
```ts [routes/sse.ts]
import { defineHandler } from "nitro";
import { createEventStream } from "nitro/h3";
export default defineHandler((event) => {
const stream = createEventStream(event);
const interval = setInterval(() => {
stream.push(`tick @ ${Date.now()}`);
}, 1000);
stream.onClosed(() => clearInterval(interval));
return stream.send();
});
```
`stream.push` also accepts structured messages: `{ id, event, data, retry }`. Connect from the client with `new EventSource("/sse")`.
## Key Points
- Enable `features.websocket: true`; `defineWebSocketHandler` is imported from `nitro`.
- Hooks: `upgrade` (auth/context), `open`, `message`, `close`, `error`.
- `peer.publish` excludes the sender; namespaces (default = pathname) isolate broadcasts.
- For one-way streaming use SSE via `createEventStream` from `nitro/h3`.
<!--
Source references:
- https://nitro.build/docs/websocket
- https://crossws.h3.dev/
-->
SKILL.md
---
name: nitro
description: Nitro is the framework-agnostic server toolkit (powering Nuxt) for building and deploying web servers anywhere. Use when working with nitro.config, server routes/event handlers, route rules, caching, storage, tasks, websockets, or deploying to Node/Bun/Deno/Cloudflare/Vercel.
metadata:
author: Anthony Fu
version: "2026.9.25"
source: Generated from https://github.com/nitrojs/nitro, scripts located at https://github.com/antfu/skills
---
> The skill is based on Nitro v3 (beta, 3.0.260903), generated at 2026-09-25.
Nitro is a framework-agnostic, deployment-agnostic server toolkit powered by [H3](https://h3.dev) v2, [unstorage](https://unstorage.unjs.io), and Vite/Rolldown/Rollup. It powers Nuxt and works standalone. From one codebase it builds optimized output for Node.js, Bun, Deno, Cloudflare, Vercel, Netlify, and more.
Key capabilities:
- **Filesystem routing** with H3 v2 event handlers, dynamic params, and method suffixes.
- **Route rules** for declarative caching, headers, redirects, proxying, and CORS.
- **Caching** layer (cached handlers/functions, SWR) on top of the **unstorage** KV layer.
- **Runtime config** overridable via `NITRO_*` env vars.
- **Tasks** (on-demand + scheduled/cron), **WebSockets**/SSE, a SQL **database** layer, and **OpenAPI** auto-docs.
- **Plugins & lifecycle hooks**, custom **renderer**/**server entry**, and portable **deployment presets**.
> **Key v3 changes** (vs. `nitropack` v2): the package is renamed `nitropack` → `nitro` with H3 v2 (web-standard `Request`/`Response`); **auto-imports are removed** (import everything explicitly); directory scanning is **opt-in** via `serverDir`; `useStorage` → `useKV` (`nitro/kv`) and the `storage` config option → `kv`; cache `swr` now defaults to `false`. If unsure about v2-vs-v3 APIs, read [advanced-migration](references/advanced-migration.md) first.
## Core
| Topic | Description | Reference |
|-------|-------------|-----------|
| Routing | File-based routes, `defineHandler`, params, middleware, route rules, errors | [core-routing](references/core-routing.md) |
| Configuration | `nitro.config.ts`, `defineConfig`, key options, runtime config | [core-configuration](references/core-configuration.md) |
| Storage | unstorage KV, mount points, drivers, dynamic mounts | [core-storage](references/core-storage.md) |
| Cache | `defineCachedHandler`, `defineCachedFunction`, SWR, invalidation | [core-cache](references/core-cache.md) |
| Assets | Public assets, compression, server assets via storage | [core-assets](references/core-assets.md) |
| Rendering | Renderer (HTML/SSR), server entry, framework integration | [core-rendering](references/core-rendering.md) |
## Features
| Topic | Description | Reference |
|-------|-------------|-----------|
| Plugins & Hooks | `definePlugin`, runtime lifecycle hooks, error capture | [features-plugins](references/features-plugins.md) |
| Tasks | On-demand & scheduled (cron) tasks, `runTask` | [features-tasks](references/features-tasks.md) |
| WebSocket & SSE | `defineWebSocketHandler`, pub/sub, namespaces, event streams | [features-websocket](references/features-websocket.md) |
| Database | Built-in SQL layer via db0, `useDatabase`, connectors | [features-database](references/features-database.md) |
| OpenAPI | Auto spec from `defineRouteMeta`, Scalar/Swagger UIs | [features-openapi](references/features-openapi.md) |
## Advanced / Deployment
| Topic | Description | Reference |
|-------|-------------|-----------|
| Deployment Presets | Runtimes & providers, compatibility dates, platform integration | [deploy-presets](references/deploy-presets.md) |
| v2 → v3 Migration | Package rename, `nitro/*` imports, H3 v2 API, preset changes | [advanced-migration](references/advanced-migration.md) |