# Technical Documentation: nitrojs/nitro

> ℹ️ **Provenance:** Multi-Tier Ingestion · [CodeWiki Reference](https://codewiki.google/github.com/nitrojs/nitro)

### Overview (`README.md`)

[](https://deepwiki.com/nitrojs/nitro)

# Nitro

> [!NOTE]
> You’re viewing the **v3** branch.
> For the current stable release, see [Nitro v2](https://github.com/nitrojs/nitro/tree/v2).

**Nitro** extends your Vite app with a **production-ready server**, designed to run **anywhere**.
Add server routes, deploy across multiple platforms, and enjoy a **zero-config** experience.

📘 **Docs:** [https://nitro.build](https://nitro.build)

## Contributing

Check out the [Contribution Guide](./CONTRIBUTING.md) to get started.

## License

Released under the [MIT License](LICENSE).

---

### Overview (`examples/api-routes/README.md`)

Nitro supports file-based routing in the `api/` or `routes/` directory. Each file becomes an API endpoint based on its path.

## Basic Route

Create a file in the `api/` directory to define a route. The file path becomes the URL path:

```ts [api/hello.ts]
import { defineHandler } from "nitro";

export default defineHandler(() => "Nitro is amazing!");
```

This creates a `GET /api/hello` endpoint.

## Dynamic Routes

Use square brackets `[param]` for dynamic URL segments. Access params via `event.context.params`:

```ts [api/hello/[name].ts]
import { defineHandler } from "nitro";

export default defineHandler((event) => `Hello (param: ${event.context.params!.name})!`);
```

This creates a `GET /api/hello/:name` endpoint (e.g., `/api/hello/world`).

## HTTP Methods

Suffix your file with the HTTP method (`.get.ts`, `.post.ts`, `.put.ts`, `.delete.ts`, etc.):

### GET Handler

```ts [api/test.get.ts]
import { defineHandler } from "nitro";

export default defineHandler(() => "Test get handler");
```

### POST Handler

```ts [api/test.post.ts]
import { defineHandler } from "nitro";

export default defineHandler(async (event) => {
  const body = await event.req.json();
  return {
    message: "Test post handler",
    body,
  };
});
```

---

### Overview (`examples/cached-handler/README.md`)

This example shows how to cache an expensive operation (a 500 ms delay) and conditionally bypass the cache using a query parameter. On first request, the handler executes and caches the result. Subsequent requests return the cached response instantly until the cache expires or is bypassed.

## How It Works

```ts [server.ts]
import { html } from "nitro";
import { defineCachedHandler } from "nitro/cache";

export default defineCachedHandler(
  async () => {
    await new Promise((resolve) => setTimeout(resolve, 500));
    return html`
      Response generated at ${new Date().toISOString()} (took 500ms)
      <br />(<a href="?skipCache=true">skip cache</a>)
    `;
  },
  { shouldBypassCache: ({ req }) => req.url.includes("skipCache=true") }
);
```

The handler simulates a slow operation with a 500ms delay. As `defineCachedHandler` wraps it, the response is cached after the first execution. The `shouldBypassCache` option checks for `?skipCache=true` in the URL and when present the cache is skipped and the handler runs fresh.

---

### Overview (`examples/custom-error-handler/README.md`)

This example shows how to intercept all errors and return a custom response format. When any route throws an error, Nitro calls your error handler instead of returning the default error page.

## Error Handler

Create an `error.ts` file in your project root to define the global error handler:

```ts [error.ts]
import { defineErrorHandler } from "nitro";

export default defineErrorHandler((error, _event) => {
  return new Response(`Custom Error Handler: ${error.message}`, {
    status: 500,
    headers: { "Content-Type": "text/plain" },
  });
});
```

The handler receives the thrown error and the H3 event object. You can use the event to access request details like headers, cookies, or the URL path to customize responses per route.

## Triggering an Error

The main handler throws an error to demonstrate the custom error handler:

```ts [server.ts]
import { defineHandler, HTTPError } from "nitro";

export default defineHandler(() => {
  throw new HTTPError("Example Error!", { status: 500 });
});
```

When you visit the page, instead of seeing a generic error page, you'll see "Custom Error Handler: Example Error!" because the error handler intercepts the thrown error.

---

### Overview (`examples/database/README.md`)

Nitro provides a built-in database layer that uses SQL template literals for safe, parameterized queries. This example creates a users table, inserts a record, and queries it back.

## Querying the Database

```ts [server.ts]
import { defineHandler } from "nitro";
import { useDatabase } from "nitro/database";

export default defineHandler(async () => {
  const db = useDatabase();

  // Create users table
  await db.sql`DROP TABLE IF EXISTS users`;
  await db.sql`CREATE TABLE IF NOT EXISTS users ("id" TEXT PRIMARY KEY, "firstName" TEXT, "lastName" TEXT, "email" TEXT)`;

  // Add a new user
  const userId = String(Math.round(Math.random() * 10_000));
  await db.sql`INSERT INTO users VALUES (${userId}, 'John', 'Doe', '')`;

  // Query for users
  const { rows } = await db.sql`SELECT * FROM users WHERE id = ${userId}`;

  return {
    rows,
  };
});
```

Retrieve the database instance using `useDatabase()`. The database can be queried using `db.sql`, and variables like `${userId}` are automatically escaped to prevent SQL injection.

## Running Migrations with Tasks

Nitro tasks let you run operations outside of request handlers. For database migrations, create a task file in `tasks/` and run it via the CLI. This keeps schema changes separate from your application code.

```ts [tasks/db/migrate.ts]
import { defineTask } from "nitro/task";
import { useDatabase } from "nitro/database";

export default defineTask({
  meta: {
    description: "Run database migrations",
  },
  async run() {
    const db = useDatabase();

    console.log("Running database migrations...");

    // Create users table
    await db.sql`DROP TABLE IF EXISTS users`;
    await db.sql`CREATE TABLE IF NOT EXISTS users ("id" TEXT PRIMARY KEY, "firstName" TEXT, "lastName" TEXT, "email" TEXT)`;

    return {
      result: "Database migrations complete!",
    };
  },
});
```

---

### Overview (`examples/elysia/README.md`)

## Server Entry

```ts [server.ts]
import { Elysia } from "elysia";

const app = new Elysia();

app.get("/", () => "Hello, Elysia with Nitro!");

export default app.compile();
```

Nitro auto-detects `server.ts` in your project root and uses it as the server entry. The Elysia app handles all incoming requests, giving you full control over routing and middleware.

Call `app.compile()` before exporting to optimize the router for production.

---

### Overview (`examples/express/README.md`)

## Server Entry

```ts [server.node.ts]
import Express from "express";

const app = Express();

app.use("/", (_req, res) => {
  res.send("Hello from Express with Nitro!");
});

export default app;
```

Nitro auto-detects `server.node.ts` in your project root and uses it as the server entry. The Express app handles all incoming requests, giving you full control over routing and middleware.

::note
The `.node.ts` suffix indicates this entry is Node.js specific and won't work in other runtimes like Cloudflare Workers or Deno.
::

---

### Overview (`examples/fastify/README.md`)

## Server Entry

```ts [server.node.ts]
import Fastify from "fastify";

const app = Fastify();

app.get("/", () => "Hello, Fastify with Nitro!");

await app.ready();

export default app.routing;
```

Nitro auto-detects `server.node.ts` in your project root and uses it as the server entry.

Call `await app.ready()` to initialize all registered plugins before exporting. Export `app.routing` (not `app`) to provide Nitro with the request handler function.

::note
The `.node.ts` suffix indicates this entry is Node.js specific and won't work in other runtimes like Cloudflare Workers or Deno.
::

---

### Overview (`examples/hello-world/README.md`)

The simplest Nitro server. Export an object with a `fetch` method that receives a standard `Request` and returns a `Response`. No frameworks, no abstractions, just the web platform.


## Server Entry

```ts [server.ts]
export default {
  fetch(req: Request) {
    return new Response("Nitro Works!");
  },
};
```

The `fetch` method follows the same signature as Service Workers and Cloudflare Workers. This pattern works across all deployment targets because it uses web standards.

Add the Nitro plugin to Vite and it handles the rest: dev server, hot reloading, and production builds.

---

### Overview (`examples/hono/README.md`)

## Server Entry

```ts [server.ts]
import { Hono } from "hono";

const app = new Hono();

app.get("/", (c) => {
  return c.text("Hello, Hono with Nitro!");
});

export default app;
```

Nitro auto-detects `server.ts` in your project root and uses it as the server entry. The Hono app handles all incoming requests, giving you full control over routing and middleware.

Hono is cross-runtime compatible, so this server entry works across all Nitro deployment targets including Node.js, Deno, Bun, and Cloudflare Workers.

---

### Overview (`examples/import-alias/README.md`)

Import aliases like `~` and `#` let you reference modules with shorter paths instead of relative imports.

## Importing Using Aliases

```ts [server/routes/index.ts]
import { sum } from "~server/utils/math.ts";

import { rand } from "#server/utils/math.ts";

export default () => {
  const [a, b] = [rand(1, 10), rand(1, 10)];
  const result = sum(a, b);
  return `The sum of ${a} + ${b} = ${result}`;
};
```

The route imports the `sum` function using `~server/` and `rand` using `#server/`. Both resolve to the same `server/utils/math.ts` file. The handler generates two random numbers and returns their sum.

## Configuration

Aliases can be configured in `package.json` imports field, `compilerOptions.paths` of `tsconfig.json` or the `alias` option of `nitro.config.ts`.

---

### Overview (`examples/middleware/README.md`)

Middleware functions run before route handlers on every request. They can modify the request, add context, or return early responses.

## Defining Middleware

Create files in `server/middleware/`. They run in alphabetical order:

```ts [server/middleware/auth.ts]
import { defineMiddleware } from "nitro";

export default defineMiddleware((event) => {
  event.context.auth = { name: "User " + Math.round(Math.random() * 100) };
});
```

Middleware can:
- Add data to `event.context` for use in handlers
- Return a response early to short-circuit the request
- Modify request headers or other properties

## Accessing Context in Handlers

Data added to `event.context` in middleware is available in all subsequent handlers:

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

export default defineHandler((event) => ({
  auth: event.context.auth,
}));
```

---

### Overview (`examples/mono-jsx/README.md`)

## Server Entry

```tsx [server.tsx]
export default () => (
  <html>
    <h1>Nitro + mono-jsx works!</h1>
  </html>
);
```

Nitro auto-detects `server.tsx` and uses mono-jsx to transform JSX into HTML. Export a function that returns JSX, and Nitro sends the rendered HTML as the response.

---

### Overview (`examples/nano-jsx/README.md`)

## Server Entry

```tsx [server.tsx]
import { defineHandler, html } from "nitro";
import { renderSSR } from "nano-jsx";

export default defineHandler(() => {
  return html(renderSSR(() => <h1>Nitro + nano-jsx works!</h1>));
});
```

Nitro auto-detects `server.tsx` and uses it as the server entry. Use `renderSSR` from nano-jsx to convert JSX into an HTML string. The `html` helper from H3 sets the correct content type header.

---

### Overview (`examples/plugins/README.md`)

Plugins let you hook into Nitro's runtime lifecycle. This example shows a plugin that modifies the `Content-Type` header on every response. Create files in `server/plugins/` and they're automatically loaded at startup.

## Defining a Plugin

```ts [server/plugins/test.ts]
import { definePlugin } from "nitro";
import { useNitroHooks } from "nitro/app";

export default definePlugin(() => {
  const hooks = useNitroHooks();
  hooks.hook("response", (res) => {
    res.headers.set("content-type", "html; charset=utf-8");
  });
});
```

The plugin uses `useNitroHooks()` to access the hooks system, then registers a `response` hook that runs after every request and receives the outgoing `Response`. Here it sets the content type to HTML, but you could log requests, add security headers, or modify responses in any way.

## Main Handler

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

export default defineHandler(() => "<h1>Hello Nitro!</h1>");
```

The handler returns HTML without setting a content type. The plugin automatically adds the correct `Content-Type: html; charset=utf-8` header to the response.

---

### Overview (`examples/renderer/README.md`)

Create a custom renderer that generates HTML responses with data from API routes. Use Nitro's internal `fetch` to call routes without network overhead.

## Renderer

```ts [renderer.ts]
import { fetch } from "nitro";

export default async function renderer({ url }: { req: Request; url: URL }) {
  const apiRes = await fetch("/api/hello").then((res) => res.text());
  return new Response(
    /* html */ `<!DOCTYPE html>
    <html>
    <head>
      <title>Custom Renderer</title>
    </head>
    <body>
      <h1>Hello from custom renderer!</h1>
      <p>Current path: ${url.pathname}</p>
      <p>API says: ${apiRes}</p>
    </body>
    </html>`,
    { headers: { "content-type": "text/html; charset=utf-8" } }
  );
}
```

Configure the renderer with `renderer: { handler: "./renderer" }` in `nitro.config.ts`, and Nitro uses it for all non-API routes. The renderer function receives the request URL and returns a `Response`. (Alternatively, if an `index.html` exists in your project root, Nitro auto-detects it as a renderer template.)

Use `fetch` from `nitro` to call API routes without network overhead—these requests stay in-process.

## API Route

```ts [api/hello.ts]
import { defineHandler } from "nitro";

export default defineHandler(() => "Nitro is amazing!");
```

Define API routes in the `api/` directory. When the renderer calls `fetch("/api/hello")`, this handler runs and returns its response.

---

### Overview (`examples/runtime-config/README.md`)

Runtime config lets you define configuration values that can be overridden by environment variables at runtime.

## Define Config Schema

Declare your runtime config with default values in `nitro.config.ts`:

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

export default defineConfig({
  serverDir: "./",
  runtimeConfig: {
    apiKey: "",
  },
});
```

## Access at Runtime

Use `useRuntimeConfig` to access configuration values in your handlers:

```ts [server.ts]
import { defineHandler } from "nitro";
import { useRuntimeConfig } from "nitro/runtime-config";

export default defineHandler((event) => {
  const runtimeConfig = useRuntimeConfig();
  return { runtimeConfig };
});
```

## Environment Variables

Override config values via environment variables prefixed with `NITRO_`:

```sh [.env]
# NEVER COMMIT SENSITIVE DATA. THIS IS ONLY FOR DEMO PURPOSES.
NITRO_API_KEY=secret-api-key
```

---

### Overview (`examples/server-fetch/README.md`)

When you need one route to call another, use Nitro's `fetch` function instead of the global fetch. It makes internal requests that stay in-process, avoiding network round-trips. The request never leaves the server.

## Main Route

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

export default defineHandler(() => fetch("/hello"));
```

The index route imports `fetch` from `nitro` (not the global fetch) and calls the `/hello` route. This request is handled internally without going through the network stack.

## Internal API Route

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

export default defineHandler(() => "Hello!");
```

A simple route that returns "Hello!". When the index route calls `fetch("/hello")`, this handler runs and its response is returned directly.

---

### Overview (`examples/shiki/README.md`)

Use Shiki for syntax highlighting with TextMate grammars. This example highlights code on the server using Nitro's server scripts feature, which runs JavaScript inside HTML files before sending the response.

## API Route

```ts [api/highlight.ts]
import { createHighlighterCore } from "shiki/core";
import { createOnigurumaEngine } from "shiki/engine/oniguruma";

const highlighter = await createHighlighterCore({
  engine: createOnigurumaEngine(import("shiki/wasm")),
  themes: [await import("shiki/themes/vitesse-dark.mjs")],
  langs: [await import("shiki/langs/ts.mjs")],
});

export default async ({ req }: { req: Request }) => {
  const code = await req.text();
  const html = await highlighter.codeToHtml(code, {
    lang: "ts",
    theme: "vitesse-dark",
  });
  return new Response(html, {
    headers: { "Content-Type": "text/html; charset=utf-8" },
  });
};
```

Create a Shiki highlighter with the Vitesse Dark theme and TypeScript language support. When the API receives a POST request, it reads the code from the request body and returns highlighted HTML.

## Server-Side Rendering

```html [index.html]
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width,initial-scale=1" />
    <title>Hello World Snippet</title>
    <link rel="stylesheet" href="styles.css" />
  </head>
  <body>
    <div class="card" role="region" aria-label="Code snippet">
      <div class="label">JavaScript</div>
      <script server>
        const hl = (code) =>
          serverFetch("/api/highlight", {
            method: "POST",
            body: code,
          });
      </script>
      <pre><code>{{{ hl(`console.log("💚 Simple is beautiful!");`) }}}</code></pre>
    </div>
  </body>
</html>
```

The `<script server>` tag runs on the server before the HTML is sent. It defines a helper function that calls the highlight API using `serverFetch`. The triple-brace syntax `{{{ }}}` outputs the result without escaping, so the highlighted HTML renders correctly.

---

### Overview (`examples/takumi/README.md`)

Generate dynamic [Open Graph](https://ogp.me/) images from a Nitro route using [Takumi](https://takumi.kane.tw). The `index.html` page references the generated image through its `og:image` meta tag and previews it live.

## Server Route

Build the node tree with Takumi [helpers](https://takumi.kane.tw/docs/helpers) — no JSX setup needed. Nitro handlers can return a `Response`, so return an `ImageResponse` directly. The handler awaits `response.ready` and adds a `Server-Timing` header so callers can see how long the render took:

```ts [routes/og.png.ts]
import { defineHandler } from "nitro";
import { container, text } from "takumi-js/helpers";
import ImageResponse from "takumi-js/response";

export default defineHandler(async ({ url }) => {
  const title = url.searchParams.get("title") ?? "Takumi + Nitro";
  const description =
    url.searchParams.get("description") ?? "Render OG images from a Nitro route.";

  const start = performance.now();

  const response = new ImageResponse(
    container({
      style: {
        width: "100%",
        height: "100%",
        display: "flex",
        flexDirection: "column",
        justifyContent: "center",
        padding: "64px",
        backgroundImage: "linear-gradient(to bottom right, #fff1f2, #fecdd3)",
      },
      children: [
        text(title, { fontSize: 72, fontWeight: 700, color: "#111827" }),
        text(description, { fontSize: 42, fontWeight: 500, color: "#4b5563" }),
      ],
    }),
    { width: 1200, height: 630 },
  );

  await response.ready;
  response.headers.set("Server-Timing", `render;dur=${(performance.now() - start).toFixed(1)}`);

  return response;
});
```

## Referencing the Image

The `index.html` points its Open Graph tags at the route so crawlers get a freshly rendered preview:

```html [index.html]
<meta property="og:image" content="/og.png?title=Takumi%20%2B%20Nitro" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
```

## Request the Endpoint

Visit `/og.png?title=Hello&description=From%20Nitro` to render an image with custom text. The response includes a `Server-Timing` header reporting the render duration; the demo page re-fetches the image as you type in the title/description fields and shows a "Generating…" / "N ms" badge overlaid in the bottom-right corner of the preview. The link icon next to the title always points at the raw endpoint for the current preview.

The `index.html` head preloads the initial image so the browser starts fetching it in parallel with parsing, and the `<img>` reserves its `1200x630` aspect ratio via width/height attributes and CSS to avoid layout shift while it loads.

Takumi picks the render backend from the deployment target: native bindings on the Node preset, WebAssembly on edge presets. No config is needed.

---

### Overview (`examples/tracing/README.md`)

Nitro can instrument its request lifecycle through Node [diagnostics channels](https://nodejs.org/api/diagnostics_channel.html) — no OpenTelemetry SDK required. This example turns instrumentation on and enables the built-in console logger, which groups every h3, srvx and unstorage span into a per-request timeline (waterfall).

## Enabling tracing

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

export default defineConfig({
  // Instrument the h3, srvx and unstorage tracing channels.
  tracingChannel: true,

  experimental: {
    // Log every completed span to the console (built-in, dependency-free sink).
    tracingLogger: true,
  },
});
```

`tracingChannel: true` wires up the producers (it accepts `{ h3, srvx, unstorage }` to trace a subset). `experimental.tracingLogger` adds a built-in sink that `console.log`s each completed span — a dependency-free alternative to a vendor exporter, handy for local development.

## Try it

```sh
npm run dev
# then, in another terminal:
curl http://localhost:3000/
curl http://localhost:3000/users/42
```

Each request prints a timeline of its spans to the console — the middleware, the matched route, and every storage operation, positioned and sized by when they ran and how long they took:

```
▶ GET /  4.10ms  (4 spans)
  middleware GET /             █·······················   0.12ms h3.handler_type=middleware http.route=/
  GET /                        ·███████████████████·····   2.49ms h3.handler_type=route http.route=/
  getItem                      ···██····················   0.18ms db.operation=getItem db.system=memory unstorage.keys_count=1
  setItem                      ·····██··················   0.16ms db.operation=setItem db.system=memory unstorage.keys_count=1
```

The header line (`▶`) is the request itself — its method, path and total time; the rows below are the spans that ran within it. Note the dynamic route is named by its matched template — `GET /users/:id`, not `/users/42` — keeping span names low-cardinality per the OpenTelemetry HTTP conventions.

The request boundary comes from Nitro's `request`/`response` runtime hooks, so grouping works identically in `vite dev` and in a production build. Spans are grouped per request via async context, so timelines for concurrent requests stay separate, and a failed span is marked `✖` with its error message.

## What gets traced

| Channel | Span | Emitted by |
| --- | --- | --- |
| `h3.request` | each matched route and middleware | `server/routes/*`, `server/middleware/*` |
| `unstorage.*` | each storage operation (`getItem`, `setItem`, …) | `useKV()` in `server/routes/index.ts` |
| `srvx.request` | the whole request, with response status (production server) | the srvx server layer |

---

### Overview (`examples/virtual-routes/README.md`)

Virtual routes let you define handlers as strings in your config instead of creating separate files. This is useful when generating routes dynamically, building plugins, or keeping simple routes inline.

## Configuration

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

export default defineConfig({
  routes: {
    "/": "#virtual-route",
  },
  virtual: {
    "#virtual-route": () =>
      /* js */ `export default () => new Response("Hello from virtual entry!")`,
  },
});
```

The `routes` option maps URL paths to virtual module identifiers (prefixed with `#`). The `virtual` option defines the module content as a string or function returning a string. At build time, Nitro resolves these virtual modules to actual handlers.

There are no route files in this project. The entire handler is defined inline in the config, and Nitro generates the route at build time.

---

### Overview (`examples/vite-nitro-plugin/README.md`)

Instead of using a separate `nitro.config.ts`, you can configure Nitro directly in your Vite config. This gives you access to Nitro's setup hook where you can register routes and virtual modules programmatically.

## Vite Configuration

```js [vite.config.mjs]
import { defineConfig } from "vite";
import { nitro } from "nitro/vite";

export default defineConfig({
  plugins: [
    nitro(),
    {
      name: "my-nitro-plugin",
      nitro: {
        setup: (nitro) => {
          nitro.options.routes["/"] = "#virtual-by-plugin";
          nitro.options.virtual["#virtual-by-plugin"] =
            `export default () => new Response("Hello from virtual entry!")`;
        },
      },
    },
  ],
});
```

The config adds two plugins: the `nitro()` plugin and a custom plugin that uses the `nitro.setup` hook. Inside the setup function, you have access to Nitro's options object. This example registers a virtual route at `/` that maps to a virtual module `#virtual-by-plugin`, then defines that module inline.

---

### Overview (`examples/vite-rsc/README.md`)

This example demonstrates React Server Components (RSC) using Vite's experimental RSC plugin with Nitro. It includes server components, client components, server actions, and streaming SSR.

## Overview

1. **SSR Entry** handles incoming requests and renders React components to HTML
2. **Root Component** defines the page structure as a server component
3. **Client Components** use the `"use client"` directive for interactive parts

## 1. SSR Entry

```
/* Detailed source-code truncated for AI context efficiency. */
```

The SSR entry handles the rendering pipeline. It loads the RSC entry module, duplicates the RSC stream (one for SSR, one for hydration), deserializes the stream back to React VDOM, and renders it to HTML. The RSC payload is injected into the HTML for client hydration.

## 2. Root Server Component

```
/* Detailed source-code truncated for AI context efficiency. */
```

Server components run only on the server. They can import CSS directly, use server-side data, and call server actions. The `ClientCounter` component is imported but runs on the client because it has the `"use client"` directive.

## 3. Client Component

```tsx [app/client.tsx]
"use client";

import React from "react";

export function ClientCounter() {
  const [count, setCount] = React.useState(0);

  return <button onClick={() => setCount((count) => count + 1)}>Client Counter: {count}</button>;
}
```

The `"use client"` directive marks this as a client component. It hydrates on the browser and handles interactive state. Server components can import and render client components, but client components cannot import server components.

---

### Overview (`examples/vite-ssr-html/README.md`)

This example renders an HTML template with server-side data and streams the response word by word. It demonstrates how to use Nitro's Vite SSR integration without a framework.

## Overview

1. **Add the Nitro Vite plugin** to enable SSR
2. **Create an HTML template** with a `` comment where server content goes
3. **Create a server entry** that fetches data and returns a stream
4. **Add API routes** for server-side data

## How It Works

The `index.html` file contains an `` comment that marks where server-rendered content will be inserted. Nitro replaces this comment with the output from your server entry.

The server entry exports an object with a `fetch` method. It calls the `/quote` API route using Nitro's internal fetch, then returns a `ReadableStream` that emits the quote text word by word with a 50ms delay between each word.

The quote route fetches a JSON file of quotes from GitHub, caches the result, and returns a random quote. The server entry calls this route to get content for the page.

---

### Overview (`examples/vite-ssr-preact/README.md`)

Set up server-side rendering (SSR) with Preact, Vite, and Nitro. This setup enables streaming HTML responses, automatic asset management, and client hydration.

## Overview

1. Add the Nitro Vite plugin to your Vite config
2. Configure client and server entry points
3. Create a server entry that renders your app to HTML
4. Create a client entry that hydrates the server-rendered HTML

## 1. Configure Vite

Add the Nitro and Preact plugins to your Vite config. Define the `client` environment with your client entry point:

```js [vite.config.mjs]
import { defineConfig } from "vite";
import { nitro } from "nitro/vite";
import preact from "@preact/preset-vite";

export default defineConfig({
  plugins: [nitro(), preact()],
  environments: {
    client: {
      build: {
        rollupOptions: {
          input: "./src/entry-client.tsx",
        },
      },
    },
  },
});
```

The `environments.client` configuration tells Vite which file to use as the browser entry point. Nitro automatically detects the SSR entry from a file named `entry-server` in `app/`, `src/`, or the project root.

## 2. Create the App Component

Create a shared Preact component that runs on both server and client:

```tsx [src/app.tsx]
import { useState } from "preact/hooks";

export function App() {
  const [count, setCount] = useState(0);
  return <button onClick={() => setCount((c) => c + 1)}>Count is {count}</button>;
}
```

## 3. Create the Server Entry

The server entry renders your Preact app to a streaming HTML response using `preact-render-to-string/stream`:

```tsx [src/entry-server.tsx]
import "./styles.css";
import { renderToReadableStream } from "preact-render-to-string/stream";
import { App } from "./app.jsx";

import clientAssets from "./entry-client?assets=client";
import serverAssets from "./entry-server?assets=ssr";

export default {
  async fetch(request: Request) {
    const url = new URL(request.url);
    const htmlStream = renderToReadableStream(<Root url={url} />);
    return new Response(htmlStream, {
      headers: { "Content-Type": "text/html;charset=utf-8" },
    });
  },
};

function Root(props: { url: URL }) {
  const assets = clientAssets.merge(serverAssets);
  return (
    <html lang="en">
      <head>
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        {assets.css.map((attr: any) => (
          <link key={attr.href} rel="stylesheet" {...attr} />
        ))}
        {assets.js.map((attr: any) => (
          <link key={attr.href} rel="modulepreload" {...attr} />
        ))}
        <script type="module" src={assets.entry} />
      </head>
      <body>
        <h1 className="hero">Nitro + Vite + Preact</h1>
        <p>URL: {props.url.href}</p>
        <div id="app">
          <App />
        </div>
      </body>
    </html>
  );
}
```

Import assets using the `?assets=client` and `?assets=ssr` query parameters. Nitro collects CSS and JS assets from each entry point, and `merge()` combines them into a single manifest. The `assets` object provides arrays of stylesheet and script attributes, plus the client entry URL. Use `renderToReadableStream` to stream HTML as Preact renders, improving time-to-first-byte.

## 4. Create the Client Entry

The client entry hydrates the server-rendered HTML, attaching Preact's event handlers:

```tsx [src/entry-client.tsx]
import { hydrate } from "preact";
import { App } from "./app.tsx";

function main() {
  hydrate(<App />, document.querySelector("#app")!);
}

main();
```

The `hydrate` function attaches Preact to the existing server-rendered DOM inside `#app` without re-rendering it.

---

### Overview (`examples/vite-ssr-react-router/README.md`)

Set up React Router framework mode with Vite and Nitro. This setup uses Nitro to run the React Router server build in development and production.

## Overview

1. Add the React Router and Nitro plugins to your Vite config
2. Configure Nitro to emit its server alongside React Router's client build
3. Create an SSR handler using React Router's request handler
4. Add Nitro server routes
5. Define routes using React Router's route configuration

## 1. Configure Vite

Add the React Router, Tailwind CSS, and Nitro plugins to your Vite config:

```ts [vite.config.ts]
import { reactRouter } from "@react-router/dev/vite";
import tailwindcss from "@tailwindcss/vite";
import { defineConfig } from "vite";
import { nitro } from "nitro/vite";

import reactRouterConfig from "./react-router.config";

export default defineConfig({
  plugins: [
    tailwindcss(),
    reactRouter(),
    nitro({
      serverDir: "./server",
      output: {
        dir: reactRouterConfig.buildDirectory,
        serverDir: `${reactRouterConfig.buildDirectory}/server`,
        publicDir: `${reactRouterConfig.buildDirectory}/client`,
      },
    }),
  ],
  resolve: {
    tsconfigPaths: true,
  },
  environments: {
    ssr: { build: { rollupOptions: { input: "./server/ssr.ts" } } },
  },
});
```

React Router creates the `ssr` environment and builds browser assets into `build/client`. The custom input points to the fetch-compatible handler in `server/ssr.ts`. Nitro scans `server/` for its own routes, then emits the production server into `build/server`.

## 2. Configure React Router

Enable SSR and keep the build directory in sync with the Nitro output configuration:

```ts [react-router.config.ts]
import type { Config } from "@react-router/dev/config";

export default {
  ssr: true,
  buildDirectory: "build",
} satisfies Config;
```

React Router generates the server build exposed by `virtual:react-router/server-build` and the client assets required for hydration.

## 3. Create the SSR Handler

Create an SSR handler that delegates incoming requests to React Router:

```ts [server/ssr.ts]
import { createRequestHandler } from "react-router";

export default {
  fetch: createRequestHandler(
    () => import("virtual:react-router/server-build"),
    import.meta.env.MODE
  ),
};
```

Nitro invokes the exported Web `fetch` handler through the Vite `ssr` service in development and production. The request handler loads React Router's generated server build and renders the matched route.

## 4. Add Nitro Server Routes

Add API and other server routes under `server/routes/`:

```ts [server/routes/health.get.ts]
import { defineHandler } from "nitro";

export default defineHandler(() => {
  return { status: "OK" };
});
```

This route is available at `/health` and is handled by Nitro before requests fall through to the React Router SSR service.

## 5. Define React Router Routes

Declare the application's route modules in `app/routes.ts`:

```ts [app/routes.ts]
import { type RouteConfig, index } from "@react-router/dev/routes";

export default [index("routes/home.tsx")] satisfies RouteConfig;
```

The index route renders the home page at `/`.

---

### Overview (`examples/vite-ssr-react/README.md`)

Set up server-side rendering (SSR) with React, Vite, and Nitro. This setup enables streaming HTML responses, automatic asset management, and client hydration.

## Overview

1. Add the Nitro Vite plugin to your Vite config
2. Configure client and server entry points
3. Create a server entry that renders your app to HTML
4. Create a client entry that hydrates the server-rendered HTML

## 1. Configure Vite

Add the Nitro and React plugins to your Vite config. Define the `client` environment with your client entry point:

```js [vite.config.mjs]
import { defineConfig } from "vite";
import { nitro } from "nitro/vite";
import react from "@vitejs/plugin-react";

export default defineConfig({
  plugins: [nitro(), react()],
  environments: {
    client: {
      build: { rollupOptions: { input: "./src/entry-client.tsx" } },
    },
  },
});
```

The `environments.client` configuration tells Vite which file to use as the browser entry point. Nitro automatically detects the SSR entry from a file named `entry-server` in `app/`, `src/`, or the project root.

## 2. Create the App Component

Create a shared React component that runs on both server and client:

```tsx [src/app.tsx]
import { useState } from "react";

export function App() {
  const [count, setCount] = useState(0);
  return (
    <>
      <h1 className="hero">Nitro + Vite + React</h1>
      <button onClick={() => setCount((c) => c + 1)}>Count is {count}</button>
    </>
  );
}
```

## 3. Create the Server Entry

The server entry renders your React app to a streaming HTML response. It uses `react-dom/server.edge` for edge-compatible streaming:

```tsx [src/entry-server.tsx]
import "./styles.css";
import { renderToReadableStream } from "react-dom/server.edge";
import { App } from "./app.tsx";

import clientAssets from "./entry-client?assets=client";
import serverAssets from "./entry-server?assets=ssr";

export default {
  async fetch(_req: Request) {
    const assets = clientAssets.merge(serverAssets);
    return new Response(
      await renderToReadableStream(
        <html lang="en">
          <head>
            <meta name="viewport" content="width=device-width, initial-scale=1.0" />
            {assets.css.map((attr: any) => (
              <link key={attr.href} rel="stylesheet" {...attr} />
            ))}
            {assets.js.map((attr: any) => (
              <link key={attr.href} rel="modulepreload" {...attr} />
            ))}
            <script type="module" src={assets.entry} />
          </head>
          <body id="app">
            <App />
          </body>
        </html>
      ),
      { headers: { "Content-Type": "text/html;charset=utf-8" } }
    );
  },
};
```

Import assets using the `?assets=client` and `?assets=ssr` query parameters. Nitro collects CSS and JS assets from each entry point, and `merge()` combines them into a single manifest. The `assets` object provides arrays of stylesheet and script attributes, plus the client entry URL. Use `renderToReadableStream` to stream HTML as React renders, improving time-to-first-byte.

## 4. Create the Client Entry

The client entry hydrates the server-rendered HTML, attaching React's event handlers:

```tsx [src/entry-client.tsx]
import "@vitejs/plugin-react/preamble";
import { hydrateRoot } from "react-dom/client";
import { App } from "./app.tsx";

hydrateRoot(document.querySelector("#app")!, <App />);
```

The `@vitejs/plugin-react/preamble` import is required for React Fast Refresh during development. The `hydrateRoot` function attaches React to the existing server-rendered DOM without re-rendering it.

---

### Overview (`examples/vite-ssr-solid/README.md`)

Set up server-side rendering (SSR) with SolidJS, Vite, and Nitro. This setup uses `renderToStringAsync` for HTML generation and supports client hydration.

## Overview

1. Add the Nitro Vite plugin to your Vite config
2. Configure client and server entry points
3. Create a server entry that renders your app to HTML
4. Create a client entry that hydrates the server-rendered HTML

## 1. Configure Vite

Add the Nitro and SolidJS plugins to your Vite config. SolidJS requires explicit JSX configuration and both `ssr` and `client` environments:

```js [vite.config.mjs]
import solid from "vite-plugin-solid";
import { defineConfig } from "vite";
import { nitro } from "nitro/vite";

export default defineConfig({
  plugins: [solid({ ssr: true }), nitro()],
  esbuild: { jsx: "preserve", jsxImportSource: "solid-js" },
  environments: {
    ssr: {
      build: { rollupOptions: { input: "./src/entry-server.tsx" } },
    },
    client: {
      build: { rollupOptions: { input: "./src/entry-client.tsx" } },
    },
  },
});
```

Enable SSR mode in the Solid plugin with `solid({ ssr: true })`. Configure esbuild to preserve JSX for Solid's compiler and use Solid's JSX runtime. SolidJS requires explicit `ssr` and `client` environment configuration in Vite.

## 2. Create the App Component

Create a shared SolidJS component using reactive signals:

```tsx [src/app.tsx]
import { createSignal } from "solid-js";

export function App() {
  const [count, setCount] = createSignal(0);

  return (
    <div>
      <h1>Hello, Solid!</h1>
      <button onClick={() => setCount((count) => count + 1)}>Count: {count()}</button>
    </div>
  );
}
```

SolidJS uses signals (`createSignal`) for state management. Unlike React's `useState`, signals are getter functions that you call to read the value.

## 3. Create the Server Entry

The server entry renders your SolidJS app to HTML using `renderToStringAsync` and includes the `HydrationScript` for client-side hydration:

```tsx [src/entry-server.tsx]
import { renderToStringAsync, HydrationScript } from "solid-js/web";
import { App } from "./app.jsx";

import clientAssets from "./entry-client?assets=client";
import serverAssets from "./entry-server?assets=ssr";

export default {
  async fetch(req: Request): Promise<Response> {
    const appHTML = await renderToStringAsync(() => <App />);
    const rootHTML = await renderToStringAsync(() => <Root appHTML={appHTML} />);
    return new Response(rootHTML, {
      headers: { "Content-Type": "text/html" },
    });
  },
};

function Root(props: { appHTML?: string }) {
  const assets = clientAssets.merge(serverAssets);
  return (
    <html lang="en">
      <head>
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        {assets.css.map((attr: any) => (
          <link key={attr.href} rel="stylesheet" {...attr} />
        ))}
        {assets.js.map((attr: any) => (
          <link key={attr.href} rel="modulepreload" {...attr} />
        ))}
      </head>
      <body>
        <div id="app" innerHTML={props.appHTML || ""} />
        <HydrationScript />
        <script type="module" src={assets.entry} />
      </body>
    </html>
  );
}
```

SolidJS requires rendering the app separately from the shell (two-phase rendering). The app HTML is injected via `innerHTML` to preserve hydration markers. Include the `HydrationScript` component to inject the script Solid needs to rehydrate on the client. Import assets using the `?assets=client` and `?assets=ssr` query parameters to collect CSS and JS from each entry point.

## 4. Create the Client Entry

The client entry hydrates the server-rendered HTML, restoring Solid's reactivity:

```tsx [src/entry-client.tsx]
import { hydrate } from "solid-js/web";
import "./styles.css";
import { App } from "./app.jsx";

hydrate(() => <App />, document.querySelector("#app")!);
```

The `hydrate` function attaches Solid's reactive system to the existing server-rendered DOM inside `#app`. The component is wrapped in a function `() => <App />` as required by Solid's API.

---

### Overview (`examples/vite-ssr-solidstart/README.md`)

Set up server-side rendering (SSR) with Solid, Vite, and Nitro. This setup enables streaming HTML responses, automatic asset management, and client hydration.

## Overview

1. Add the Nitro Vite plugin to your Vite config
2. Create a server entry that renders your app to HTML
3. Create a client entry that hydrates the server-rendered HTML

## 1. Configure Vite

Add the SolidStart and Nitro plugins to your Vite config.

```js [vite.config.ts]
import { defineConfig } from "vite";
import { solidStart } from "@solidjs/start/config";
import { nitro } from "nitro/vite";

export default defineConfig({
  plugins: [solidStart(), nitro()],
});
```

## 2. Create the App Component

Create a shared Solid component that runs on both server and client:

```tsx [src/app.tsx]
import { MetaProvider, Title } from "@solidjs/meta";
import { Router } from "@solidjs/router";
import { FileRoutes } from "@solidjs/start/router";
import { Suspense } from "solid-js";

export default function App() {
  return (
    <Router
      root={(props) => (
        <MetaProvider>
          <Title>SolidStart - Basic</Title>
          <Suspense>{props.children}</Suspense>
        </MetaProvider>
      )}
    >
      <FileRoutes />
    </Router>
  );
}
```

## 3. Create the Server Entry

The server entry renders your Solid app to a streaming HTML response:

```tsx [src/entry-server.tsx]
// @refresh reload
import { createHandler, StartServer } from "@solidjs/start/server";

export default createHandler(() => (
  <StartServer
    document={({ assets, children, scripts }) => (
      <html lang="en">
        <head>
          <meta charset="utf-8" />
          <meta name="viewport" content="width=device-width, initial-scale=1" />
          {assets}
        </head>
        <body>
          <div id="app">{children}</div>
          {scripts}
        </body>
      </html>
    )}
  />
));
```

## 4. Create the Client Entry

The client entry hydrates the server-rendered HTML, attaching Solid's event handlers:

```tsx [src/entry-client.tsx]
// @refresh reload
import { mount, StartClient } from "@solidjs/start/client";

mount(() => <StartClient />, document.getElementById("app")!);
```

---

### Overview (`examples/vite-ssr-tsr-react/README.md`)

Set up TanStack Router with React, Vite, and Nitro. This setup provides file-based routing with type-safe navigation and automatic code splitting.

## Overview

1. Add the Nitro Vite plugin to your Vite config
2. Create an HTML template with your app entry
3. Create a main entry that initializes the router
4. Define routes using file-based routing

## 1. Configure Vite

Add the Nitro, React, and TanStack Router plugins to your Vite config:

```js [vite.config.mjs]
import { defineConfig } from "vite";
import { nitro } from "nitro/vite";
import react from "@vitejs/plugin-react";
import { tanstackRouter } from "@tanstack/router-plugin/vite";

export default defineConfig({
  plugins: [tanstackRouter({ target: "react", autoCodeSplitting: true }), react(), nitro()],
});
```

The `tanstackRouter` plugin generates a route tree from your `routes/` directory structure. Enable `autoCodeSplitting` to automatically split routes into separate chunks. Place the TanStack Router plugin before the React plugin in the array.

## 2. Create the HTML Template

Create an HTML file that serves as your app shell:

```html [index.html]
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Nitro + TanStack Router + React</title>
  </head>

  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>
```

## 3. Create the App Entry

Create the main entry that initializes TanStack Router:

```tsx [src/main.tsx]
import { StrictMode } from "react";
import ReactDOM from "react-dom/client";
import { RouterProvider, createRouter } from "@tanstack/react-router";

// Import the generated route tree
import { routeTree } from "./routeTree.gen.ts";

// Create a new router instance
const router = createRouter({ routeTree });

// Register the router instance for type safety
declare module "@tanstack/react-router" {
  interface Register {
    router: typeof router;
  }
}

// Render the app
const rootElement = document.querySelector("#root")!;
if (!rootElement.innerHTML) {
  const root = ReactDOM.createRoot(rootElement);
  root.render(
    <StrictMode>
      <RouterProvider router={router} />
    </StrictMode>
  );
}
```

The `routeTree.gen.ts` file is auto-generated from your `routes/` directory structure. The `Register` interface declaration provides full type inference for route paths and params. The `!rootElement.innerHTML` check prevents re-rendering during hot module replacement.

## 4. Create the Root Route

The root route (`__root.tsx`) defines your app's layout:

```tsx [src/routes/__root.tsx]
import { createRootRoute, Link, Outlet } from "@tanstack/react-router";
import { TanStackRouterDevtools } from "@tanstack/react-router-devtools";

const RootLayout = () => (
  <>
    <div className="p-2 flex gap-2">
      <Link to="/" className="[&.active]:font-bold">
        Home
      </Link>
    </div>
    <hr />
    <Outlet />
    <TanStackRouterDevtools />
  </>
);

export const Route = createRootRoute({ component: RootLayout });
```

Use `Link` for type-safe navigation with active state styling. The `Outlet` component renders child routes. Include `TanStackRouterDevtools` for development tools (automatically removed in production).

## 5. Create Page Routes

Page routes use `createFileRoute` and can include loaders:

```tsx [src/routes/index.tsx]
import { createFileRoute } from "@tanstack/react-router";

export const Route = createFileRoute("/")({
  loader: async () => {
    const r = await fetch("/api/hello");
    return r.json();
  },
  component: Index,
});

function Index() {
  const r = Route.useLoaderData();

  return (
    <div className="p-2">
      <h3>{JSON.stringify(r)}</h3>
    </div>
  );
}
```

Fetch data before rendering with the `loader` function—data is available via `Route.useLoaderData()`. File paths determine URL paths: `routes/index.tsx` maps to `/`, `routes/about.tsx` to `/about`, and `routes/users/$id.tsx` to `/users/:id`.

---

### Overview (`examples/vite-ssr-tss-react/README.md`)

Set up TanStack Start with Nitro for a full-stack React framework experience with server-side rendering, file-based routing, and integrated API routes.

## Overview

1. Add the Nitro Vite plugin to your Vite config
2. Create a server entry using TanStack Start's server handler
3. Configure the router with default components
4. Define routes and API endpoints using file-based routing

## 1. Configure Vite

Add the Nitro, React, TanStack Start, and Tailwind plugins to your Vite config:

```js [vite.config.mjs]
import { defineConfig } from "vite";
import { nitro } from "nitro/vite";
import { tanstackStart } from "@tanstack/react-start/plugin/vite";
import viteReact from "@vitejs/plugin-react";
import tailwindcss from "@tailwindcss/vite";

export default defineConfig({
  plugins: [tanstackStart(), viteReact(), tailwindcss(), nitro()],
  resolve: { tsconfigPaths: true },
  environments: {
    ssr: { build: { rollupOptions: { input: "./server.ts" } } },
  },
});
```

The `tanstackStart()` plugin provides full SSR integration with automatic client entry handling. The `environments.ssr` option points to the server entry file.

## 2. Create the Server Entry

Create a server entry that uses TanStack Start's handler:

```ts [server.ts]
import handler, { createServerEntry } from "@tanstack/react-start/server-entry";

export default createServerEntry({
  fetch(request) {
    return handler.fetch(request);
  },
});
```

TanStack Start handles SSR automatically. The `createServerEntry` wrapper integrates with Nitro's server entry format, and the `handler.fetch` processes all incoming requests.

## 3. Configure the Router

Create a router factory function with default error and not-found components:

```tsx [src/router.tsx]
import { createRouter } from "@tanstack/react-router";
import { routeTree } from "./routeTree.gen.ts";

export function getRouter() {
  const router = createRouter({
    routeTree,
    defaultPreload: "intent",
    defaultErrorComponent: () => <div>Internal Server Error</div>,
    defaultNotFoundComponent: () => <div>Not Found</div>,
    scrollRestoration: true,
  });
  return router;
}
```

The router factory configures preloading behavior, scroll restoration, and default error/not-found components.

## 4. Create the Root Route

The root route defines your HTML shell with head management and scripts:

```tsx [src/routes/__root.tsx]
/// <reference types="vite/client" />
import { HeadContent, Link, Scripts, createRootRoute } from "@tanstack/react-router";
import { TanStackRouterDevtools } from "@tanstack/react-router-devtools";
import * as React from "react";
import appCss from "~/styles/app.css?url";

export const Route = createRootRoute({
  head: () => ({
    meta: [
      { charSet: "utf8" },
      { name: "viewport", content: "width=device-width, initial-scale=1" },
    ],
    links: [{ rel: "stylesheet", href: appCss }],
    scripts: [{ src: "/customScript.js", type: "text/javascript" }],
  }),
  errorComponent: () => <h1>500: Internal Server Error</h1>,
  notFoundComponent: () => <h1>404: Page Not Found</h1>,
  shellComponent: RootDocument,
});

function RootDocument({ children }: { children: React.ReactNode }) {
  return (
    <html>
      <head>
        <HeadContent />
      </head>
      <body>
        <div className="p-2 flex gap-2 text-lg">
          <Link to="/" activeProps={{ className: "font-bold" }} activeOptions={{ exact: true }}>
            Home
          </Link>{" "}
          <Link
            // @ts-ignore
            to="/this-route-does-not-exist"
            activeProps={{ className: "font-bold" }}
          >
            404
          </Link>
        </div>
        <hr />
        {children}
        <TanStackRouterDevtools position="bottom-right" />
        <Scripts />
      </body>
    </html>
  );
}
```

Define meta tags, stylesheets, and scripts in the `head()` function. The `shellComponent` provides the HTML document shell that wraps all pages. Use `HeadContent` to render the head configuration and `Scripts` to inject the client-side JavaScript for hydration.

## 5. Create Page Routes

Page routes define your application pages:

```tsx [src/routes/index.tsx]
import { createFileRoute } from "@tanstack/react-router";

export const Route = createFileRoute("/")({ component: Home });

function Home() {
  return (
    <div className="p-2">
      <h3>Welcome Home!</h3>
      <a href="/api/test">/api/test</a>
    </div>
  );
}
```

## API Routes

TanStack Start supports API routes alongside page routes. Create files in `src/routes/api/` to define server endpoints that Nitro serves automatically.

---

### Overview (`examples/vite-ssr-vue-router/README.md`)

Set up server-side rendering (SSR) with Vue, Vue Router, Vite, and Nitro. This setup enables per-route code splitting, head management with unhead, and client hydration.

## Overview

1. Add the Nitro Vite plugin to your Vite config
2. Define routes with lazy-loaded components
3. Create a server entry that renders your app with router support
4. Create a client entry that hydrates and takes over routing
5. Create page components

## 1. Configure Vite

Add the Nitro and Vue plugins to your Vite config. Define both `client` and `ssr` environments:

```js [vite.config.mjs]
import vue from "@vitejs/plugin-vue";
import { defineConfig } from "vite";
import devtoolsJson from "vite-plugin-devtools-json";
import { nitro } from "nitro/vite";

export default defineConfig((_env) => ({
  plugins: [patchVueExclude(vue(), /\?assets/), devtoolsJson(), nitro()],
  environments: {
    client: { build: { rollupOptions: { input: "./app/entry-client.ts" } } },
    ssr: { build: { rollupOptions: { input: "./app/entry-server.ts" } } },
  },
}));

// Workaround https://github.com/vitejs/vite-plugin-vue/issues/677
function patchVueExclude(plugin, exclude) {
  const original = plugin.transform.handler;
  plugin.transform.handler = function (...args) {
    if (exclude.test(args[1])) return;
    return original.call(this, ...args);
  };
  return plugin;
}
```

The `patchVueExclude` helper prevents the Vue plugin from processing asset imports (files with `?assets` query parameter).

## 2. Define Routes

Create route definitions with lazy-loaded components and asset metadata:

```ts [app/routes.ts]
import type { RouteRecordRaw } from "vue-router";

export const routes: RouteRecordRaw[] = [
  {
    path: "/",
    name: "app",
    component: () => import("./app.vue"),
    meta: {
      assets: () => import("./app.vue?assets"),
    },
    children: [
      {
        path: "/",
        name: "home",
        component: () => import("./pages/index.vue"),
        meta: {
          assets: () => import("./pages/index.vue?assets"),
        },
      },
      {
        path: "/about",
        name: "about",
        component: () => import("./pages/about.vue"),
        meta: {
          assets: () => import("./pages/about.vue?assets"),
        },
      },
      {
        path: "/:catchAll(.*)",
        name: "not-found",
        component: () => import("./pages/not-found.vue"),
        meta: {
          assets: () => import("./pages/not-found.vue?assets"),
        },
      },
    ],
  },
];
```

Use dynamic imports for lazy-loaded components to enable code splitting. The `meta.assets` function loads route-specific CSS and JS chunks. Define child routes under a root layout component for nested routing.

## 3. Create the Server Entry

The server entry renders your Vue app with router support and head management:

```
/* Detailed source-code truncated for AI context efficiency. */
```

The server uses `createMemoryHistory()` since there's no browser URL bar—the router navigates to the requested URL before rendering. Assets are loaded dynamically based on matched routes, ensuring only the CSS and JS needed for the current page are included. The `unhead` library manages `<head>` elements, injecting stylesheets and scripts via `transformHtmlTemplate`.

## 4. Create the Client Entry

The client entry hydrates the server-rendered HTML and takes over routing:

```ts [app/entry-client.ts]
import { createSSRApp } from "vue";
import { RouterView, createRouter, createWebHistory } from "vue-router";
import { routes } from "./routes.ts";

async function main() {
  const app = createSSRApp(RouterView);
  const router = createRouter({ history: createWebHistory(), routes });
  app.use(router);

  await router.isReady();
  app.mount("#root");
}

// eslint-disable-next-line unicorn/prefer-top-level-await
main();
```

The client entry creates a Vue app with `createWebHistory()` for browser-based routing. After the router is ready, it mounts to the `#root` element and hydrates the server-rendered HTML.

## 5. Create the Root Component

The root component provides navigation and renders child routes:

```vue [app/app.vue]
<script setup lang="ts">
import { RouterLink, RouterView } from "vue-router";
import "./styles.css";
</script>

<template>
  <nav>
    <ul>
      <li>
        <RouterLink to="/" exact-active-class="active">Home</RouterLink>
      </li>
      <li>
        <RouterLink to="/about" active-class="active">About</RouterLink>
      </li>
    </ul>
  </nav>
  <RouterView />
</template>

<style scoped>
nav {
  background: white;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
  padding: 1rem;
}

nav ul {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  gap: 2rem;
  max-width: 800px;
  margin: 0 auto;
}

nav a {
  color: #666;
  text-decoration: none;
}

nav a:hover {
  color: #333;
}

nav a.active {
  color: #646cff;
}
</style>
```

---

### Overview (`examples/vite-trpc/README.md`)

Set up tRPC with Vite and Nitro for end-to-end typesafe APIs without code generation. This example builds a counter with server-side rendering for the initial value and client-side updates.

## Overview

1. Configure Vite with the Nitro plugin and route tRPC requests
2. Create a tRPC router with procedures
3. Create an HTML page with server-side rendering and client interactivity

## 1. Configure Vite

Add the Nitro plugin and configure the `/trpc/**` route to point to your tRPC handler:

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

export default defineConfig({
  plugins: [
    nitro({
      routes: {
        "/trpc/**": "./server/trpc.ts",
      },
    }),
  ],
});
```

The `routes` option maps URL patterns to handler files. All requests to `/trpc/*` are handled by the tRPC router.

## 2. Create the tRPC Router

Define your tRPC router with procedures and export it as a fetch handler:

```ts [server/trpc.ts]
import { initTRPC } from "@trpc/server";
import { fetchRequestHandler } from "@trpc/server/adapters/fetch";

let counter = 0;

const t = initTRPC.create();

export const appRouter = t.router({
  get: t.procedure.query(() => {
    return { value: counter };
  }),

  inc: t.procedure.mutation(() => {
    counter++;
    return { value: counter };
  }),
});

export type AppRouter = typeof appRouter;

export default {
  async fetch(request: Request): Promise<Response> {
    return fetchRequestHandler({
      endpoint: "/trpc",
      req: request,
      router: appRouter,
    });
  },
};
```

Define procedures using `t.procedure.query()` for read operations and `t.procedure.mutation()` for write operations. Export the `AppRouter` type so clients get full type inference. The default export uses tRPC's fetch adapter to handle incoming requests.

## 3. Create the HTML Page

Create an HTML page with server-side rendering and client-side interactivity:

```
/* Detailed source-code truncated for AI context efficiency. */
```

The `<script server>` block runs on the server before sending the response, fetching the initial counter value via `serverFetch`. The `<script setup>` block runs in the browser and handles the increment button click.

---

### Overview (`examples/vitest/README.md`)

Test your server routes with [Vitest](https://vitest.dev). Vitest picks up the `nitro()` plugin from `vite.config.ts`, so test files run with the same routes, runtime config and storage as your server, without starting a dev server.

## Routes

```ts [api/hello.ts]
import { defineHandler } from "nitro";
import { useRuntimeConfig } from "nitro/runtime-config";

export default defineHandler(() => {
  const { greeting } = useRuntimeConfig();
  return { message: `${greeting} from Nitro!` };
});
```

```ts [api/visits.ts]
import { defineHandler } from "nitro";
import { useKV } from "nitro/kv";

export default defineHandler(async () => {
  const kv = useKV<number>();
  const visits = ((await kv.getItem("visits")) || 0) + 1;
  await kv.setItem("visits", visits);
  return { visits };
});
```

## Tests

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

beforeEach(async () => {
  await useKV().removeItem("visits");
});

test("GET /api/hello", async () => {
  const res = await serverFetch("/api/hello");
  expect(res.status).toBe(200);
  expect(await res.json()).toEqual({ message: "Hello from Nitro!" });
});

test("GET /api/visits", async () => {
  await serverFetch("/api/visits");
  const res = await serverFetch("/api/visits");
  expect(await res.json()).toEqual({ visits: 2 });
  expect(await useKV().getItem("visits")).toBe(2);
});
```

`serverFetch` sends a request through your Nitro app in-process. Runtime utilities like `useKV` work in tests too, so you can prepare or inspect state directly.

Run the tests with `vitest`, or `vitest run` for a single run.

---

### Overview (`examples/websocket/README.md`)

This example implements a simple chat room using WebSockets. Clients connect, send messages, and receive messages from other users in real-time. The server broadcasts messages to all connected clients using pub/sub channels.

## WebSocket Handler

Create a WebSocket route using `defineWebSocketHandler`.

```ts [routes/_ws.ts]
import { defineWebSocketHandler } from "nitro";

export default defineWebSocketHandler({
  open(peer) {
    peer.send({ user: "server", message: `Welcome ${peer}!` });
    peer.publish("chat", { user: "server", message: `${peer} joined!` });
    peer.subscribe("chat");
  },
  message(peer, message) {
    if (message.text().includes("ping")) {
      peer.send({ user: "server", message: "pong" });
    } else {
      const msg = {
        user: peer.toString(),
        message: message.toString(),
      };
      peer.send(msg); // echo
      peer.publish("chat", msg);
    }
  },
  close(peer) {
    peer.publish("chat", { user: "server", message: `${peer} left!` });
  },
});
```

Different hooks are exposed by `defineWebSocketHandler()` to integrate with different parts of the websocket lifecycle.

---

### Overview (`src/shims/README.md`)

Bundle-time replacements for the optional dependencies of the libraries Nitro bundles (see the
`resolve.alias` entries in `build.config.ts`).

Nitro does not declare those packages as (peer) dependencies, so a bare `import("<id>")` inside a
bundled library cannot be resolved from Nitro's own `dist/`. Each shim resolves the real package
from the user project instead, installing it on demand.

Each shim is aliased by its file name (`<id>.ts`). Its supported `version` range is listed under
`shims` in `deps.json` (synced by `pnpm sync-deps`) and must stay within the range the bundled
`library` accepts (checked by `test/unit/deps.test.ts`). These packages are not on demand
dependencies of Nitro itself (the `deps` of the manifest).

---

### Index (`docs/index.md`)

---
seo:
  title: Build Full-Stack Servers
  description: Nitro builds production-ready servers that run anywhere. Write API routes, then deploy the same codebase to Node.js, Bun, Deno, or serverless, with zero configuration.
---

::page-hero



#title
Build [Servers]{.text-brand}

#description
Nitro builds production-ready servers that run anywhere.

Write API routes, then deploy the same codebase to Node.js, Bun, Deno, or serverless, with zero configuration.

#links
:app-hero-links
::

::stat-strip
---
# `animateFrom` is only where each counter starts spinning. It is a visual
# effect with no meaning, never a real measurement.
stats:
  - value: "3.8 kB"
    label: Gzipped output
    animateFrom: 100
  - value: "0"
    label: Runtime deps
    animateFrom: 35
  - value: "25+"
    label: Deploy targets
    animateFrom: 0
  - value: "~50 ms"
    label: Cold start
    animateFrom: 350
---
::

::feature-section
---
eyebrow: Routing
link: /docs/routing
link-label: Routing docs
points:
  - Routes are compiled no runtime router ships in the bundle
  - Dynamic params, wildcards and per-method files
  - Route groups organize files without changing URLs
---
#title
Files in, Routes out

#description
Drop a file in `routes/` and it becomes a route. Append the HTTP method to the filename to scope it, nest folders for path params, and wrap a folder in parentheses to group routes without touching the URL.

#visual
  :::route-map
  ---
  dir: routes/
  routes:
    - file: hello.get.ts
      method: GET
      route: /hello
    - file: hello.post.ts
      method: POST
      route: /hello
    - file: api/test.ts
      route: /api/test
    - file: api/[org]/[repo].ts
      route: /api/:org/:repo
    - file: (admin)/users.ts
      route: /users
  ---
  :::
::

::feature-section
---
eyebrow: Server entry
reverse: true
link: /docs/server-entry
link-label: Server entry docs
---
#title
Bring your own framework

#description
Any framework that speaks the Web `fetch(request): Response` interface can be your server entry export it from `server.ts` and it runs for every request before routes are matched. Node-style `(req, res)` frameworks work too: name the file `server.node.ts` and Nitro adapts it.

#visual
  :::tabs
    ::::tab{label="H3" icon="i-unjs-h3"}
    ```ts [server.ts]
    import { H3 } from "h3";

    const app = new H3();

    app.get("/", () => "⚡️ Hello from H3!");

    export default app;
    ```
    ::::

    ::::tab{label="Hono" icon="i-logos-hono"}
    ```ts [server.ts]
    import { Hono } from "hono";

    const app = new Hono();

    app.get("/", (c) => c.text("🔥 Hello from Hono!"));

    export default app;
    ```
    ::::

    ::::tab{label="Elysia" icon="i-skill-icons-elysia-dark"}
    ```ts [server.ts]
    import { Elysia } from "elysia";

    const app = new Elysia();

    app.get("/", () => "🦊 Hello from Elysia!");

    export default app.compile();
    ```
    ::::

    ::::tab{label="Express" icon="i-simple-icons-express"}
    ```ts [server.node.ts]
    import Express from "express";

    const app = Express();

    app.use("/", (_req, res) => {
      res.send("Hello from Express with Nitro!");
    });

    export default app;
    ```
    ::::
  :::
::

::feature-section
---
eyebrow: Deploy
link: /deploy
link-label: All deploy targets
points:
  - Providers are auto-detected in CI no adapter to install
  - Switch targets with a single `preset` option
  - Compatibility dates keep provider behavior stable over time
---
#title
One codebase, every platform

#description
The same server builds for Node.js, Deno, Bun, edge workers and serverless functions. Nitro emits the output format each host expects, so moving between them requires no code changes!

#visual
  :::deploy-grid
  ---
  more: 25+ targets
  moreTo: /deploy
  targets:
    - name: Node.js
      icon: i-simple-icons-nodedotjs
      to: /deploy/runtimes/node
    - name: Deno
      icon: i-simple-icons-deno
      to: /deploy/runtimes/deno
    - name: Bun
      icon: i-simple-icons-bun
      to: /deploy/runtimes/bun
    - name: Cloudflare
      icon: i-simple-icons-cloudflare
      to: /deploy/providers/cloudflare
    - name: Vercel
      icon: i-simple-icons-vercel
      to: /deploy/providers/vercel
    - name: Netlify
      icon: i-simple-icons-netlify
      to: /deploy/providers/netlify
    - name: AWS Lambda
      icon: i-simple-icons-awslambda
      to: /deploy/providers/aws
    - name: Azure
      icon: i-simple-icons-microsoftazure
      to: /deploy/providers/azure
    - name: Firebase
      icon: i-simple-icons-firebase
      to: /deploy/providers/firebase
    - name: DigitalOcean
      icon: i-simple-icons-digitalocean
      to: /deploy/providers/digitalocean
    - name: Render
      icon: i-simple-icons-render
      to: /deploy/providers/render
  ---
  :::
::

::feature-section
---
eyebrow: Output
reverse: true
link: /docs/quick-start
link-label: Build your first server
---
#title
Small enough to read

#description
A production build of a minimal Nitro server is three files with nothing to install alongside them. Dependencies are bundled and tree-shaken, so what you deploy is the code you wrote plus the little that runs it.

#visual
  :::bar-chart
  ---
  title: Production server output
  data:
    - label: Unminified
      value: 16.2
      display: 16.2 kB
    - label: Minified
      value: 8.8
      display: 8.8 kB
    - label: Minified + gzipped
      value: 3.8
      display: 3.8 kB
      highlight: true
  ---
  :::
::

::feature-section
---
eyebrow: Cache
link: /docs/cache
link-label: Caching docs
points:
  - Stale-while-revalidate responses by default
  - ETag, last-modified and 304 handling out of the box
  - Concurrent requests for one key share a single invocation
---
#title
Caching that follows your storage

#description
Wrap a handler or any async function and Nitro caches its result on the same storage layer your app already uses memory in development, then Redis, Cloudflare KV, a Vercel Blob store or the filesystem in production. Same code, different backend.

#visual
  ```ts [routes/stars.ts]
  import { defineCachedHandler } from "nitro/cache";

  export default defineCachedHandler(
    async () => {
      const res = await fetch("https://api.github.com/repos/nitrojs/nitro");
      const { stargazers_count } = await res.json();
      return { stars: stargazers_count };
    },
    { maxAge: 60 * 60 }
  );
  ```
::

::feature-grid
---
eyebrow: Included
title: Everything a server needs
description: The pieces most apps reach for ship with Nitro, and every one of them behaves the same on every deployment target.
features:
  - title: KV Storage
    description: One key-value API over the filesystem, Redis, Cloudflare KV and more.
    icon: i-lucide-hard-drive
    to: /docs/storage
  - title: Database
    description: SQL layer powered by db0, preconfigured with SQLite.
    icon: i-lucide-database
    badge: Experimental
    to: /docs/database
  - title: Tasks
    description: One-off runtime operations, run from the CLI or on a cron schedule.
    icon: i-lucide-list-checks
    badge: Experimental
    to: /docs/tasks
  - title: WebSockets
    description: Cross-runtime WebSocket support built on crossws.
    icon: i-lucide-radio
    to: /docs/websocket
  - title: Plugins
    description: Hook into the server lifecycle from the plugins/ directory.
    icon: i-lucide-plug
    to: /docs/plugins
  - title: OpenAPI
    description: A spec generated from your handlers, served through Scalar or Swagger UI.
    icon: i-lucide-file-json
    badge: Experimental
    to: /docs/openapi
  - title: Assets
    description: Public files served directly, server assets readable at runtime.
    icon: i-lucide-image
    to: /docs/assets
  - title: Renderer
    description: A catch-all handler for unmatched routes SSR, SPA shells or plain HTML.
    icon: i-lucide-layout-template
    to: /docs/renderer
  - title: Lifecycle
    description: Every layer a request passes through, in order, and where to intercept it.
    icon: i-lucide-activity
    to: /docs/lifecycle
---
::

::call-to-action{command="create-nitro-app"}
#title
Start with one command

#description
Scaffold a project, or add the Vite plugin to the app you already have.

#actions
:app-hero-links
::

::sponsors

---

### 1.Docs 1.Index (`docs/1.docs/1.index.md`)

---
icon: i-lucide-compass
---

# Introduction

> Nitro is a full-stack server framework, compatible with any runtime and any deployment target.

Nitro gives you a production-ready server with filesystem routing, code-splitting, and built-in support for storage, caching, and databases, all runtime-agnostic and deployable anywhere.

## What is Nitro?

Create server and API routes inside the `routes/` directory. Each file maps directly to a URL path, and Nitro handles the rest: [routing](/docs/routing), code-splitting, and optimized builds.

You can also take full control by creating a `server.ts` [server entry](/docs/server-entry). Nitro’s runtime-agnostic approach lets you use any HTTP library, such as [Elysia](https://elysiajs.com/), [H3](https://h3.dev), or [Hono](https://hono.dev).

### Performance

Nitro compiles your routes at build time, removing the need for a runtime router. Only the code required to handle each incoming request is loaded and executed. This makes it ideal for serverless hosting, with near-0ms boot time regardless of project size.

### Deploy Anywhere

Build your server into an optimized `.output/` folder compatible with Node.js, Bun, Deno, and many hosting platforms without any configuration: Cloudflare Workers, Netlify, Vercel, and more. Take advantage of platform features like ESR, ISR, and SWR without changing a single line of code.

### Server-Side Rendering

Render HTML with your favorite templating engine, or use component libraries such as React, Vue, or Svelte directly on the server. Go full universal rendering with client-side hydration. Nitro provides the foundation and a progressive approach to reach your goals.

### Storage

Nitro includes a runtime-agnostic key-value [storage layer](/docs/storage) out of the box. It uses in-memory storage by default, but you can connect more than 20 different drivers (FS, Redis, S3, etc.), attach them to different namespaces, and swap them without changing your code.

### Caching

Nitro supports [caching](/docs/cache) for both server routes and server functions, backed directly by the server storage (via the `cache` namespace).

### Database

Nitro also includes a built-in SQL [database](/docs/database). It defaults to SQLite, but you can connect to and query more than 10 databases (Postgres, MySQL, PGLite, etc.) using the same API.

### Meta-Framework Foundation

Nitro can be used as the foundation for building your own meta-framework. Popular frameworks such as Nuxt, SolidStart, and TanStack Start fully or partially leverage Nitro.

## Vite Integration

Nitro integrates seamlessly with [Vite](https://vite.dev) as a plugin. If you’re building a frontend application with Vite, adding Nitro gives you API routes, server-side rendering, and a full production server, all built together with `vite build`.

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

export default defineConfig({
  plugins: [nitro()],
});
```

With Nitro, `vite build` produces an optimized `.output/` folder containing both your frontend and backend, ready to deploy anywhere.

Ready to give it a try? Jump into the [quick start](/docs/quick-start).

---

### 2.Deploy 0.Index (`docs/2.deploy/0.index.md`)

---
icon: ri:upload-cloud-2-line
---

# Deploy

> Learn more about Nitro deploy providers.

Nitro can generate different output formats for different hosting providers from the same code base.
Using built-in presets, you can adjust the output format with little to no extra code or configuration.

## Default output

The default production output preset is [Node.js server](/deploy/runtimes/node).

In development mode, Nitro always uses a special `nitro-dev` preset that runs your app with Node.js (ESM) in an isolated worker environment, matching production behavior as closely as possible.

## Zero-Config Providers

When deploying to production using CI/CD, Nitro tries to detect the provider environment and set the right preset automatically, with no configuration required. The following providers can be auto-detected with zero config:

- [AWS Amplify](/deploy/providers/aws-amplify)
- [Azure](/deploy/providers/azure)
- [Cloudflare](/deploy/providers/cloudflare)
- [Firebase App Hosting](/deploy/providers/firebase#firebase-app-hosting)
- [Netlify](/deploy/providers/netlify)
- [Stormkit](/deploy/providers/stormkit)
- [Vercel](/deploy/providers/vercel)
- [Zeabur](/deploy/providers/zeabur)

::warning
For Turborepo users, its Strict Environment Mode can interfere with zero-config detection. You may need to allow the detection variables explicitly or use Loose Environment Mode (the `--env-mode=loose` flag).
::

Other built-in providers are available with an explicit preset, including [Zephyr](/deploy/providers/zephyr).

## Changing the deployment preset

To build for a specific provider, set the `NITRO_PRESET` environment variable, update your Nitro [configuration](/docs/configuration), or use the `--preset` argument.

The environment variable approach is recommended for CI/CD deployments.

**Example:** Using the `NITRO_PRESET` environment variable or the `--preset` argument
```bash
NITRO_PRESET=cloudflare_pages nitro build
# or
nitro build --preset cloudflare_pages
```

**Example:** Updating the `nitro.config.ts` file

```ts
import { defineConfig } from "nitro";

export default defineConfig({
  preset: 'cloudflare_pages'
})
```

## Compatibility date

Deployment providers regularly update their runtime behavior. Nitro presets are updated to support these new features.

To prevent breaking existing deployments, Nitro uses compatibility dates. These dates let you lock in behavior at project creation time. You can also opt in to future updates when ready.

When you create a new project, the `compatibilityDate` is set to the current date. This setting is saved in your project's configuration.

You should update the compatibility date periodically. Always test your deployment thoroughly after updating. Below is a list of key dates and their effects.

---

### 3.Config 0.Index (`docs/3.config/0.index.md`)

---
icon: ri:settings-3-line
---

# Config

> A reference of every option available in `nitro.config.ts`.

:read-more{to="/docs/configuration"}

::note
All examples on this page assume the following import in `nitro.config.ts`:

```ts
import { defineConfig } from "nitro";
```
::

## General

Core options for presets, logging, and runtime configuration.

### `preset`

The deployment preset to build for in **production**. Can also be set with the `NITRO_PRESET` environment variable.

The preset for development mode is always `nitro_dev`. For production, the default is `node_server`, which builds a standalone Node.js server.

When the `preset` option is not set and Nitro is running in a known environment, the preset is detected automatically.

```ts
export default defineConfig({
  preset: "cloudflare_pages", // deploy to Cloudflare Pages
});
```

### `defaultPreset`

Customize the preset used as the **fallback** when `preset` is not set and none of the known hosting providers are auto-detected.

By default, Nitro falls back to the runtime-based preset (`node`, and `deno` or `bun` when running on those runtimes). An explicit `preset`, the `NITRO_PRESET` environment variable, and auto-detected providers (Vercel, Netlify, Cloudflare Pages, ...) all take precedence over `defaultPreset`.

It accepts a preset name or an inline preset definition.

```ts
export default defineConfig({
  defaultPreset: "node_cluster", // use node_cluster instead of node_server by default
});
```

### `debug`

- Default: `false`{lang=ts} (`true`{lang=ts} when `DEBUG` environment variable is set)

Enable debug mode for verbose logging and additional development information.

```ts
export default defineConfig({
  debug: true,
});
```

### `logLevel`

- Default: `3`{lang=ts} (`1`{lang=ts} when the testing environment is detected)

Log verbosity level. See [consola](https://github.com/unjs/consola?tab=readme-ov-file#log-level) for more information.

```ts
export default defineConfig({
  logLevel: 4, // verbose logging
});
```

### `runtimeConfig`

- Default: `{ app: {}, nitro: {} }`{lang=ts}

Server runtime configuration. Nitro injects `app.baseURL` (from the [`baseURL`](#baseurl) option) into the `app` namespace.

**Note:** The `app` and `nitro` namespaces are reserved for internal use.

```ts
export default defineConfig({
  runtimeConfig: {
    apiSecret: "default-secret", // override with NITRO_API_SECRET
  },
});
```

### `compatibilityDate`

- Default: `"latest"`{lang=ts} behavior when not provided

Deployment providers introduce new features that Nitro presets can leverage, but some of them need to be explicitly opted into. Set this to the latest date you have tested against, in `YYYY-MM-DD` format, to leverage the latest preset features.

```ts
export default defineConfig({
  compatibilityDate: "2025-01-01",
});
```

### `static`

- Default: `false`{lang=ts}

Enable static site generation mode.

```ts
export default defineConfig({
  static: true, // prerender all routes
});
```

## Features

Enable and configure Nitro's built-in capabilities such as storage, tasks, and assets.

### `features`

- Default: `{}`{lang=ts}

Enable built-in features.

#### `runtimeHooks`

- Default: auto-detected (enabled if there is at least one nitro plugin)

Enable runtime hooks for request and response.

#### `websocket`

- Default: `false`{lang=ts}

Enable WebSocket support.

```ts
export default defineConfig({
  features: {
    runtimeHooks: true,
    websocket: true, // enable WebSocket support
  },
});
```

:read-more{to="/docs/websocket"}

### `experimental`

- Default: `{}`{lang=ts}

Enable experimental features.

#### `openAPI`

- Default: `false`{lang=ts}

Enable `/_scalar`, `/_swagger` and `/_openapi.json` endpoints.

::note
Prefer using the top-level [`openAPI`](#openapi) option for configuration.
::

#### `typescriptBundlerResolution`

Enable TypeScript bundler module resolution. See [TypeScript#51669](https://github.com/microsoft/TypeScript/pull/51669).

::note
This flag currently has no effect: bundler module resolution is always used by `nitro/tsconfig` and the generated tsconfig.
::

#### `asyncContext`

Enable native async context support for `useRequest()`.

#### `sourcemapMinify`

Set to `false` to disable experimental sourcemap minification (enabled by default when `sourcemap` is enabled).

#### `envExpansion`

Allow env expansion in runtime config. See the [configuration guide](/docs/configuration#env-expansion) and [#2043](https://github.com/nitrojs/nitro/pull/2043).

#### `database`

Enable experimental database support. See [Database](/docs/database).

#### `tasks`

Enable experimental tasks support. See [Tasks](/docs/tasks).

#### `tracingLogger`

Log Nitro tracing-channel spans to the console using a built-in, dependency-free telemetry sink that logs each completed span (h3, srvx, unstorage, ...). Requires [`tracingChannel`](#tracingchannel) to be enabled.

```ts
export default defineConfig({
  experimental: {
    typescriptBundlerResolution: true,
    asyncContext: true,
    envExpansion: true,
    database: true,
    tasks: true,
  },
});
```

### `openAPI`

Top-level OpenAPI configuration.

You can pass an object to modify your OpenAPI specification:

```js
openAPI: {
  meta: {
    title: 'My Awesome Project',
    description: 'This might become the next big thing.',
    version: '1.0'
  }
}
```

These routes are disabled by default in production. To enable them, use the `production` key.
`"runtime"` allows middleware usage, and `"prerender"` is the most efficient because the JSON response is constant.

```js
openAPI: {
    // IMPORTANT: make sure to protect OpenAPI routes if necessary!
    production: "runtime", // or "prerender"
}
```

To customize the Scalar integration, [pass a configuration object](https://github.com/scalar/scalar) like this:

```js
openAPI: {
  ui: {
    scalar: {
      theme: 'purple'
    }
  }
}
```

To customize the Swagger UI, pass any [Swagger UI configuration option](https://swagger.io/docs/open-source-tools/swagger-ui/usage/configuration/):

```js
openAPI: {
  ui: {
    swagger: {
      persistAuthorization: true,
      deepLinking: true,
      docExpansion: 'none',
      filter: true,
    }
  }
}
```

To customize the endpoints:

```js
openAPI: {
  route: "/_docs/openapi.json",
  ui: {
    scalar: {
      route: "/_docs/scalar"
    },
    swagger: {
      route: "/_docs/swagger"
    }
  }
}
```

:read-more{to="/docs/openapi"}

### `kv`

- Default: `{}`{lang=ts}

KV storage configuration.

```ts
export default defineConfig({
  kv: {
    redis: {
      driver: "redis",
      url: "redis://localhost:6379",
    },
  },
});
```

Use the `$development` key to override mounts during development:

```ts
export default defineConfig({
  $development: {
    kv: {
      redis: {
        driver: "fs",
        base: "./data/redis", // use filesystem in development
      },
    },
  },
});
```

`$development` only applies to the development server. Use `$prerender` to override mounts while prerendering. A mount with a different `driver` replaces the whole mount, and one with the same `driver` has its options deep merged.

::note
Previously named `storage`, which still works as a deprecated alias. The deprecated `devStorage` option is replaced by `kv` inside `$development` (and `$prerender`).
::

:read-more{to="/docs/storage"}

### `cache`

- Default: `{}`{lang=ts}

Cache storage and global default options for [cached functions, handlers](/docs/cache) and `cache` route rules.

```ts
export default defineConfig({
  cache: {
    driver: "fs", // "memory" (default) | "fs" | "kv"
    fs: { dir: ".data/cache" },
    defaults: { maxAge: 60, swr: true },
  },
});
```

- `driver`: `memory` (in-process LRU), `fs` (filesystem, Node.js compatible runtimes only) or `kv` (the [`kv`](#kv) storage layer). Defaults to `kv` when a `cache` KV mount point is configured, and `memory` otherwise.
- `memory`: `maxSize` and `maxBytes` limits of the `memory` driver.
- `fs`: `dir` of the `fs` driver (defaults to `.data/cache`).
- `defaults`: global default options for every cache (`maxAge`, `swr`, `staleMaxAge`, `maxResolveTime`, `base`, and handler-only `varies`, `allowQuery`, `allowCookies`, `allowAuthorization`, `sendCacheControl`, `cacheStatusHeader`, `maxBodySize`).

:read-more{to="/docs/cache#cache-storage"}

### `database`

Database connection configurations. Requires `experimental.database: true`.

```ts
export default defineConfig({
  database: {
    default: {
      connector: "sqlite",
      options: { name: "db" },
    },
  },
});
```

:read-more{to="/docs/database"}

### `devDatabase`

Database connection configuration overrides for development mode.

```ts
export default defineConfig({
  devDatabase: {
    default: {
      connector: "sqlite",
      options: { name: "db-dev" }, // separate dev database
    },
  },
});
```

### `renderer`

- Type: `false`{lang=ts} | `{ handler?: string, static?: boolean, template?: string }`{lang=ts}

Points to main render entry (file should export an event handler as default).

```ts
export default defineConfig({
  renderer: {
    handler: "~/renderer", // path to the render handler
  },
});
```

:read-more{to="/docs/renderer"}

### `ssrRoutes`

Routes that should be server-side rendered.

```ts
export default defineConfig({
  ssrRoutes: ["/app/**"],
});
```

### `serveStatic`

- Type: `boolean`{lang=ts} | `'node'`{lang=ts} | `'inline'`{lang=ts}
- Default: depends on the deployment preset used.

Serve `public/` assets in production.

**Note:** It is highly recommended that your edge CDN (Nginx, Apache, Cloud) serves the `.output/public/` directory instead to enable compression and higher level caching.

```ts
export default defineConfig({
  serveStatic: "node", // serve static assets using Node.js
});
```

### `noPublicDir`

- Default: `false`{lang=ts}

If enabled, disables `.output/public` directory creation. Skips copying `public/` dir and also disables pre-rendering.

```ts
export default defineConfig({
  noPublicDir: true, // skip public directory output
});
```

### `publicAssets`

Public asset directories to serve in development and bundle in production.

If a `public/` directory is detected, it will be added by default, but you can add more by yourself too!

It's possible to set Cache-Control headers for assets using the `maxAge` option:

```ts
export default defineConfig({
  publicAssets: [
    {
      baseURL: "images",
      dir: "public/images",
      maxAge: 60 * 60 * 24 * 7, // 7 days
    },
  ],
});
```

The config above generates the following header in the assets under `public/images/` folder:

`cache-control: public, max-age=604800, immutable`

The `dir` option is where your files live on your file system; the `baseURL` option is the folder they will be accessible from when served/bundled.

:read-more{to="/docs/assets"}

### `compressPublicAssets`

- Type: `boolean`{lang=ts} | `{ gzip?: boolean, brotli?: boolean, zstd?: boolean }`{lang=ts}
- Default: `false`{lang=ts}

If enabled, Nitro will generate a pre-compressed (gzip, brotli, and/or zstd) version of supported types of public assets and prerendered routes
larger than 1024 bytes into the public directory. Default compression levels are used. Using this option you can support zero overhead asset compression without using a CDN.

```ts
export default defineConfig({
  compressPublicAssets: {
    gzip: true,
    brotli: true, // enable gzip and brotli pre-compression
  },
});
```

### `serverAssets`

Assets can be accessed in server logic and bundled in production.

```ts
export default defineConfig({
  serverAssets: [
    {
      baseName: "templates",
      dir: "./templates", // bundle templates/ as server assets
    },
  ],
});
```

:read-more{to="/docs/assets#server-assets"}

### `modules`

- Default: `[]`{lang=ts}

An array of Nitro modules. Modules can be a string (path), a module object with a `setup` function, or a function.

```ts
export default defineConfig({
  modules: [
    "./modules/my-module.ts",
    (nitro) => {
      nitro.hooks.hook("compiled", () => { /* ... */ });
    },
  ],
});
```

### `plugins`

- Default: `[]`{lang=ts}

An array of paths to nitro plugins. They will be executed in order on the first initialization.

Note that Nitro auto-registers the plugins in the `plugins/` directory.

```ts
export default defineConfig({
  plugins: [
    "~/plugins/my-plugin.ts",
  ],
});
```

:read-more{to="/docs/plugins"}

### `tasks`

- Default: `{}`{lang=ts}

Task definitions. Each key is a task name with an optional `handler` path and `description`.

Tasks scanned from `<serverDir>/tasks/` only need a `description` here. A `handler` is imported as-is and is not resolved against the project root, so it has to be an absolute path or a package name.

```ts
import { fileURLToPath } from "node:url";

export default defineConfig({
  tasks: {
    "db:migrate": {
      description: "Run database migrations",
    },
    "db:seed": {
      handler: fileURLToPath(new URL("scripts/seed.ts", import.meta.url)),
      description: "Seed the database",
    },
  },
});
```

:read-more{to="/docs/tasks"}

### `scheduledTasks`

- Default: `{}`{lang=ts}

Map of cron expressions to task name(s).

```ts
export default defineConfig({
  scheduledTasks: {
    "0 * * * *": "cleanup:temp",
    "*/5 * * * *": ["health:check", "metrics:collect"],
  },
});
```

:read-more{to="/docs/tasks"}

### `virtual`

- Default: `{}`{lang=ts}

A map from dynamic virtual import names to their contents or an (async) function that returns it.

```ts
export default defineConfig({
  virtual: {
    "#config": `export default { version: "1.0.0" }`,
  },
});
```

### `ignore`

- Default: `[]`{lang=ts}

Array of glob patterns to ignore when scanning directories.

```ts
export default defineConfig({
  ignore: [
    "routes/_legacy/**", // skip legacy route handlers
  ],
});
```

Test files (`*.test.ts`, `*.spec.ts`) and type declarations (`*.d.ts`) are always ignored.

### `scanSuffixes`

- Default: `[]`{lang=ts}

Additional dotted suffixes to keep as part of scanned file names. Nitro reserves trailing lowercase suffixes (like `.get` or `.dev`) in scanned file names. Known file extensions (e.g. `.json`, `.md`, `.xml`) are always kept. See [reserved suffixes](/docs/routing#reserved-suffixes).

```ts
export default defineConfig({
  scanSuffixes: ["prompt"], // routes/system.prompt.ts -> /system.prompt
});
```

### `wasm`

- Type: `false`{lang=ts} | `UnwasmPluginOptions`{lang=ts}
- Default: `{}`{lang=ts}

WASM support configuration. See [unwasm](https://github.com/unjs/unwasm) for options.

```ts
export default defineConfig({
  wasm: {}, // enable WASM import support
});
```

## Dev

Options that only affect the development server (`nitro dev`).

### `devServer`

- Default: `{ watch: [] }`{lang=ts}

Dev server options. You can use `watch` to make the dev server reload if any file changes in specified paths.

Supports `port`, `hostname`, `watch`, `runner`, and `devtoolsJson` options.

```ts
export default defineConfig({
  devServer: {
    port: 3001,
    watch: ["./server/plugins"],
  },
});
```

By default, the dev server serves `/.well-known/appspecific/com.chrome.devtools.json` so Chrome DevTools can connect your project folder as an [automatic workspace](https://chromium.googlesource.com/devtools/devtools-frontend/+/main/docs/ecosystem/automatic_workspace_folders.md) and save edits back to source files. The workspace UUID is persisted in `buildDir`. Set `devServer.devtoolsJson: false` to disable it.

### `watchOptions`

Watch options for development mode. See [chokidar](https://github.com/paulmillr/chokidar) for more information.

```ts
export default defineConfig({
  watchOptions: {
    ignored: ["**/node_modules/**", "**/dist/**"],
  },
});
```

### `devProxy`

Proxy configuration for development server.

You can use this option to override development server routes and proxy-pass requests.

```ts
export default defineConfig({
  devProxy: {
    "/proxy/test": "http://localhost:3001",
    "/proxy/example": { target: "https://example.com", changeOrigin: true },
    "/proxy/socket/**": { target: "http://localhost:3002", ws: true },
  },
});
```

Routes match like other Nitro routes: `/proxy/test` only matches that exact path, use `/proxy/test/**` to also proxy sub-paths. Set `ws: true` to also proxy WebSocket upgrade requests.

See [httpxy](https://github.com/unjs/httpxy) for all available target options.

::note
When the dev server runs on Bun or Deno (e.g. `bun --bun nitro dev`), it listens with the runtime's native server. HTTP requests are then proxied with `proxyFetch` from httpxy, supporting `target`, `headers`, `auth`, `xfwd`, `changeOrigin`, `secure`, `ssl`, `agent`, `prependPath`, `ignorePath`, `proxyTimeout` and `followRedirects`. WebSockets are proxied with the [crossws](https://crossws.h3.dev) proxy, supporting `target`, `headers`, `auth`, `xfwd`, `prependPath` and `ignorePath` (the upstream `Host` header always matches the target). WebSocket upgrades to the dev worker of the `miniflare` runner (Cloudflare presets) are only supported on Node.js.
::

## Logging

### `logging`

- Default: `{ compressedSizes: true, buildSuccess: true }`{lang=ts}

Control build logging behavior. Set `compressedSizes` to `false` to skip reporting compressed bundle sizes. Set `buildSuccess` to `false` to suppress the build success message.

```ts
export default defineConfig({
  logging: {
    compressedSizes: false, // skip compressed size reporting
    buildSuccess: false,
  },
});
```

## Routing

Options for defining routes, handlers, and route-level behavior.

### `baseURL`

- Default: `"/"`{lang=ts} (or `NITRO_APP_BASE_URL` environment variable if provided)

Server's main base URL.

```ts
export default defineConfig({
  baseURL: "/app/", // serve app under /app/ prefix
});
```

### `apiBaseURL`

- Default: `"/api"`{lang=ts}

Changes the default API base URL prefix.

```ts
export default defineConfig({
  apiBaseURL: "/server/api", // api routes under /server/api/
});
```

### `serverEntry`

- Type: `false`{lang=ts} | `string`{lang=ts} | `{ handler: string, format?: EventHandlerFormat }`{lang=ts}

Custom server entry point configuration. Set to `false` to disable the default server entry.

```ts
export default defineConfig({
  serverEntry: "./server.ts",
});
```

:read-more{to="/docs/server-entry"}

### `handlers`

Server handlers and routes.

If `routes/`, `api/` or `middleware/` directories exist inside the server directory, they will be automatically added to the handlers array.

```ts
export default defineConfig({
  handlers: [
    { route: "/health", handler: "./handlers/health.ts" },
    { route: "/admin/**", handler: "./handlers/admin.ts", method: "get" },
  ],
});
```

:read-more{to="/docs/routing"}

### `devHandlers`

Handlers provided as programmatic instances instead of file paths. Unlike regular [`handlers`](#handlers), which are imported and transformed by the bundler, `devHandlers` accept a handler function directly.

**Note:** They are **only available in development mode** and **not in the production build**.

```ts
export default defineConfig({
  devHandlers: [
    { route: "/__dev", handler: defineHandler(() => "dev-only route") },
  ],
});
```

### `routes`

- Default: `{}`{lang=ts}

Inline route definitions. A map from route pattern to handler path or handler options.

```ts
export default defineConfig({
  routes: {
    "/hello": "./routes/hello.ts",
    "/greet": { handler: "./routes/greet.ts", method: "post" },
  },
});
```

### `errorHandler`

- Type: `string`{lang=ts} | `string[]`{lang=ts}

Path(s) to custom runtime error handler(s). Replaces nitro's built-in error page.

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

export default defineConfig({
  errorHandler: "~/error",
});
```

```ts [error.ts]
import { defineErrorHandler } from "nitro";

export default defineErrorHandler((error, event) => {
  return new Response('[custom error handler] ' + error.stack, {
    headers: { 'Content-Type': 'text/plain' }
  });
});
```

### `routeRules`

**🧪 Experimental!**

Route options. It is a map from route pattern (following [rou3](https://github.com/h3js/rou3)) to route options.

When `cache` option is set, handlers matching pattern will be automatically wrapped with `defineCachedHandler`.

See the [Cache API](/docs/cache) for all available cache options.

::note
`swr: true|number` is shortcut for `cache: { swr: true, maxAge: number }`
::

```js
routeRules: {
  '/blog/**': { swr: true },
  '/feed/**': { swr: 600 },
  '/docs/**': { static: true },
  '/dashboard/**': { cache: { /* cache options*/ } },
  '/assets/**': { headers: { 'cache-control': 's-maxage=0' } },
  '/api/v1/**': { cors: true, headers: { 'access-control-allow-methods': 'GET' } },
  '/old-page': { redirect: '/new-page' }, // uses status code 307 (Temporary Redirect)
  '/old-page2': { redirect: { to:'/new-page2', statusCode: 301 } },
  '/old-page/**': { redirect: '/new-page/**' },
  '/proxy/example': { proxy: 'https://example.com' },
  '/proxy/**': { proxy: '/api/**' },
}
```

:read-more{to="/docs/routing#route-rules"}

### `prerender`

Prerendering options. Any route specified will be fetched during the build and copied to the `.output/public` directory as a static asset.

Default:

```ts
{
  autoSubfolderIndex: true,
  concurrency: 1,
  interval: 0,
  failOnError: false,
  crawlLinks: false,
  ignore: [],
  routes: []
}
```

Any route (string) that starts with a prefix listed in `ignore`, or matches a regular expression or function, will be ignored.

If the `crawlLinks` option is set to `true`, nitro starts with `/` by default (or all routes in the `routes` array), extracts `<a>` tags from HTML pages, and prerenders them as well.

You can set the `failOnError` option to `true` to stop the CI when Nitro could not prerender a route.

The `interval` and `concurrency` options let you control the speed of prerendering, which can be useful to avoid rate limits when calling external APIs.

The `autoSubfolderIndex` option controls how files are generated in the `.output/public` directory:

```bash
# autoSubfolderIndex: true (default)
/about -> .output/public/about/index.html
# autoSubfolderIndex: false
/about -> .output/public/about.html
```

This option is useful when your hosting provider does not give you an option regarding the trailing slash.

Set `ignoreUnprefixedPublicAssets` to `true` to skip prerendering public assets that do not have a base URL prefix.

## Directories

Customize where Nitro looks for source files and writes its output.

### `workspaceDir`

Project workspace root directory.

The workspace directory (e.g. pnpm workspace) is automatically detected when the `workspaceDir` option is not set.

```ts
export default defineConfig({
  workspaceDir: "../", // monorepo root
});
```

### `rootDir`

Project main directory.

```ts
export default defineConfig({
  rootDir: "./src/server",
});
```

### `serverDir`

- Type: `boolean`{lang=ts} | `"./"`{lang=ts} | `"./server"`{lang=ts} | `string`{lang=ts}
- Default: `false`{lang=ts}

Server directory for scanning `api/`, `routes/`, `plugins/`, `utils/`, `middleware/`, `modules/`, and `tasks/` folders.

When set to `false`, automatic directory scanning is disabled. Set to `"./"` to use the root directory, or `"./server"` to use a `server/` subdirectory.

```ts
export default defineConfig({
  serverDir: "./server", // scan server/ subdirectory
});
```

### `scanDirs`

- Default: `[]`{lang=ts} (the server source directory is scanned when empty)

List of directories to scan and auto-register files, such as API routes.

```ts
export default defineConfig({
  scanDirs: ["./modules/auth/api", "./modules/billing/api"],
});
```

### `apiDir`

- Default: `"api"`{lang=ts}

Defines a different directory to scan for api route handlers.

```ts
export default defineConfig({
  apiDir: "endpoints", // scan endpoints/ instead of api/
});
```

### `routesDir`

- Default: `"routes"`{lang=ts}

Defines a different directory to scan for route handlers.

```ts
export default defineConfig({
  routesDir: "pages", // scan pages/ instead of routes/
});
```

### `buildDir`

- Default: `"node_modules/.nitro"`{lang=ts}

Nitro's temporary working directory for generating build-related files.

```ts
export default defineConfig({
  buildDir: ".nitro", // use .nitro/ in project root
});
```

### `output`

- Default: `{ dir: '.output', serverDir: '.output/server', publicDir: '.output/public' }`{lang=ts}

Output directories for production bundle.

```ts
export default defineConfig({
  output: {
    dir: "dist",
    serverDir: "dist/server",
    publicDir: "dist/public",
  },
});
```

## Build

Bundler selection and build output tuning.

### `builder`

- Type: `"rollup"`{lang=ts} | `"rolldown"`{lang=ts} | `"vite"`{lang=ts} | `false`{lang=ts}
- Default: `undefined`{lang=ts} (auto-detected)

Specify the bundler to use for building.

```ts
export default defineConfig({
  builder: "vite",
});
```

When not set, Nitro uses `vite` if your `vite.config` uses the `nitro()` plugin, otherwise `rolldown`. The builder package is resolved from your project (`rolldown` also through your `vite`) and installed on demand when building. In development, if `rolldown` is not installed and no [`rollupConfig`](#rollupconfig) or [`rolldownConfig`](#rolldownconfig) is set, `nitro dev` runs without a builder (`builder: false`) and `nitro build` offers to install it.

Set `builder: false` (experimental) to run the server sources without bundling. `nitro dev` then runs your files directly in an [env-runner](https://github.com/unjs/env-runner) worker, Nitro's virtual modules are served from memory, and a fresh worker starts on each change. [`nitro start`](/docs/cli#nitro-serve) (or `nitro serve`) runs the sources the same way in production mode, without a build step, whatever builder is configured. `nitro preview` only serves a build output. [Prerendering](/docs/prerendering) runs the sources the same way, and is also used by the `vite` builder when `rolldown` is not installed. `nitro build` is not supported yet.

Modules are transformed on demand, like in a bundled build: TypeScript and JSX, `import.meta.*` and [`replace`](#replace) values, [`alias`](#alias), extensionless imports, `raw:`/`text`/`bytes` imports and WebAssembly.

::note
- On Bun, aliases without a file extension (such as `~/utils`) and WebAssembly imports are not resolved yet.
- Files outside the project, its scan dirs and Nitro (such as linked workspace packages) are loaded as-is, without these transforms.
- Bundler options (`rollupConfig`, `rolldownConfig`) and plugins added by modules with the `build:config` hook do not apply. Use [`buildPlugins`](#buildplugins) for plugins.
::

### `buildPlugins`

- Type: `NitroBuildPluginOption[]`{lang=ts}
- Default: `[]`{lang=ts}

Build plugins applied with every [builder](#builder) (`rollup`, `rolldown`, `vite` and `false`). Nested arrays and promises are resolved, falsy entries are skipped.

Plugins use the Rollup plugin interface. Prefer the `resolveId`, `load` and `transform` hooks with a `filter`: they are the ones supported by all builders.

Plugins with `enforce: "pre"` are placed before Nitro's own plugins, the others after them (`enforce: "post"` last). A hook's own `order` takes precedence: several Nitro hooks (such as virtual module resolution) use `order: "pre"`, so use it too for a hook that must run before them.

::note
- Builders differ in what runs around the plugins. `transform` hooks receive TypeScript and JSX untransformed with `rolldown` (it compiles them after the plugins) and for `enforce: "pre"` plugins with `rollup`. With `vite`, Vite's own resolution and transforms run first.
- With `vite`, plugins apply to the `nitro` environment only, as bundler plugins: Vite-specific options and hooks (such as `apply` or `configureServer`) are not supported. Add Vite plugins to Vite's `plugins` instead.
- With `builder: false`, plugins run in [env-runner](https://github.com/unjs/env-runner), which supports only `resolveId`, `load` and `transform` (with a limited plugin context) and warns about other hooks. Hooks only receive JavaScript and TypeScript modules unless their `filter` names other files (by `id` or `moduleType`), and files in `node_modules` only when it names `node_modules`.
::

```ts
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!"`,
      },
    },
  ],
});
```

Nitro modules can add plugins with `nitro.options.buildPlugins.push(plugin)`.

### `rollupConfig`

Additional rollup configuration.

```ts
export default defineConfig({
  rollupConfig: {
    output: { manualChunks: { vendor: ["lodash-es"] } },
  },
});
```

### `rolldownConfig`

Additional rolldown configuration.

```ts
export default defineConfig({
  rolldownConfig: {
    output: { banner: "/* built with nitro */" },
  },
});
```

### `vite`

Options for the `vite` builder and the `nitro/vite` plugin.

- `path`: The `vite` package to use, as a path or `file://` URL to its directory or entry (same as [`deps.paths.vite`](#deps)). By default, `vite` is resolved from the project root. A framework running Vite programmatically should pass its own `vite` (e.g. `import.meta.resolve("vite")`) so the dev module runner matches the running instance.
- `services`: Additional Vite environments to register as fetchable services, as `{ [name]: { entry } }`. Server environments with an entry (such as `ssr`) are registered automatically.
- `assetsImport`: Enable `?assets` imports for collecting CSS and JS assets from entry points (default: `true`).
- `serverReload`: Reload the dev server (and the browser) when a server-only module changes (default: `true`).

```ts
export default defineConfig({
  vite: {
    path: import.meta.resolve("vite"),
    services: { api: { entry: "./src/api-entry.ts" } },
  },
});
```

### `deps`

Options for the optional dependencies Nitro installs on demand (such as `vite`, `rolldown`, `rollup`, `unstorage`, `db0` or `unenv`).

- `paths`: Explicit packages to use, by package name. Each value is a path or `file://` URL to the package directory or entry. Relative paths resolve against [`rootDir`](#rootdir).

By default, these packages are resolved from your project and installed on demand when missing. A framework built on top of Nitro can pass its own copies in `deps.paths` instead, so the versions it depends on are the ones that run. The server bundle uses them too for `unstorage`, `db0` and `unenv`. An explicit package is never installed, and Nitro warns when its version is outside the supported range. Nothing is installed either when [`rootDir`](#rootdir) is inside `node_modules` (e.g. a framework using its own package directory): the framework has to declare the packages it uses in its own dependencies or pass them in `deps.paths`.

```ts
export default defineConfig({
  deps: {
    paths: {
      unstorage: import.meta.resolve("unstorage"),
      db0: import.meta.resolve("db0"),
    },
  },
});
```

`deps.paths.vite` is an alias of [`vite.path`](#vite).

### `entry`

Bundler entry point.

```ts
export default defineConfig({
  entry: "./server/entry.ts", // custom entry file
});
```

### `alias`

Path aliases for module resolution.

```ts
export default defineConfig({
  alias: {
    "~utils": "./src/utils",
    "#shared": "./shared",
  },
});
```

### `inject`

Globals to replace with imports when referenced in the bundle. Values are a module id (default export) or a `[id, exportName]` tuple.

Set a global to `false` to remove a default injection (e.g. one added for [`node: false`](#node) builds).

```ts
export default defineConfig({
  inject: {
    Buffer: ["node:buffer", "Buffer"],
    performance: false,
  },
});
```

### `polyfills`

Modules imported for their side effects before the server entry. Prefix an entry with `!` to remove a default polyfill.

[`node: false`](#node) builds add `#nitro/runtime/polyfills/global`, `#nitro/runtime/polyfills/process`, `#nitro/runtime/polyfills/buffer` and `#nitro/runtime/polyfills/timers` by default (e.g. `"!#nitro/runtime/polyfills/timers"` removes the `setImmediate` polyfill).

```ts
export default defineConfig({
  polyfills: ["./polyfills/intl.ts"],
});
```

### `builtinModules`

Modules provided by the target runtime, such as `cloudflare:workers` or `node:fs` on Deno. Imports of these ids are kept as-is in the output.

Builtin modules are never bundled, traced, or copied to the output, so only list modules that the runtime itself provides. To keep an npm package out of the bundle, use [`traceDeps`](#tracedeps) instead. Node.js builtins are handled automatically when [`node`](#node) is `true`.

Prefix an entry with `!` to remove a default (e.g. one added by a preset).

```ts
export default defineConfig({
  builtinModules: ["cloudflare:workers"],
});
```

::note
Presets also add default `alias`, `inject`, `polyfills`, and `builtinModules` entries (for example, Cloudflare and Deno map Node.js builtins they support natively). Your own values in these options always take precedence.

The `unenv` option is deprecated. Presets passed to it still apply, after preset defaults and before your own options.
::

### `minify`

- Default: `false`{lang=ts}

Minify bundle.

Some presets (especially worker runtimes) enable minification by default. Set `minify: false` when debugging production, worker, or edge builds so stack traces stay readable. 

```ts
export default defineConfig({
  minify: true, // minify production bundle
});
```

```ts
export default defineConfig({
  minify: false, // easier to debug workers / edge bundles
});
```

### `inlineDynamicImports`

- Default: `false`{lang=ts}

Bundle all code into a single file instead of creating separate chunks per route.

When `false`, each route handler becomes a separate chunk loaded on-demand. When `true`, everything is bundled together. Some presets enable this by default.

```ts
export default defineConfig({
  inlineDynamicImports: true, // single output file
});
```

### `sourcemap`

- Default: `false`{lang=ts}

Enable source map generation. See [options](https://rollupjs.org/configuration-options/#output-sourcemap).

```ts
export default defineConfig({
  sourcemap: true, // generate .map files
});
```

### `node`

- Default: `true`{lang=ts}

Specify whether the build is used for Node.js or not. If set to `false`, nitro adds default [`alias`](#alias), [`inject`](#inject) and [`polyfills`](#polyfills) entries that polyfill Node.js globals and built-in modules (using [unenv](https://github.com/unjs/unenv)), and adjusts its behavior.

Node.js built-in modules that the target runtime does not provide (e.g. `node:inspector` on Cloudflare) are polyfilled with [unenv](https://github.com/unjs/unenv). `unenv` is resolved from your project and installed on demand, only when the build imports such a module.

```ts
export default defineConfig({
  node: false, // target non-Node.js runtimes
});
```

### `replace`

Build-time string replacements.

```ts
export default defineConfig({
  replace: {
    "process.env.APP_VERSION": JSON.stringify("1.0.0"),
  },
});
```

### `commonJS`

Specifies additional configuration for the rollup CommonJS plugin.

```ts
export default defineConfig({
  commonJS: {
    requireReturnsDefault: "auto",
  },
});
```

### `exportConditions`

Custom export conditions for module resolution.

```ts
export default defineConfig({
  exportConditions: ["worker", "production"],
});
```

### `noExternals`

- Default: `false`{lang=ts}

Prevent packages from being externalized. Externalized packages are imported at runtime instead of being bundled, so they do not see your `alias` configuration.

In a production build Nitro already bundles almost everything: only native or otherwise non-bundleable packages and the ones listed in [`traceDeps`](#tracedeps) stay external. In development, dependencies inside `node_modules` are kept external for speed, which is where this option matters most.

Set it to `true` to bundle everything, including packages that are normally kept external:

```ts
export default defineConfig({
  noExternals: true, // bundle all dependencies
});
```

#### Inlining specific packages

Pass an array to inline only some modules. `RegExp` entries are used as-is; string entries are escaped and matched as a path substring.

Each pattern is tested against the import specifier as written (for example `@scope/my-lib`) and against the resolved absolute file path (for example `/…/node_modules/@scope/my-lib/dist/index.mjs`). A match on either one keeps the module bundled, so a plain package name is usually all you need:

```ts
export default defineConfig({
  alias: {
    "@shared": "./shared",
  },
  noExternals: ["@scope/my-lib"],
});
```

Use a path pattern when you need to be more precise, for example to inline one package in a monorepo without matching a similarly named one:

```ts
export default defineConfig({
  noExternals: [/packages[\\/]my-lib/],
});
```

Write `[\\/]` instead of `/` in such patterns so they also match Windows paths.

::note
In Nitro 2 this option was `externals.inline`.
::

### `traceDeps`

- Default: `[]`{lang=ts}

Additional dependencies to trace and include in the build output.

Supports special prefixes:
- `!pkg`: Exclude a built-in package from tracing.
- `pkg*`: Full trace, copies all package files instead of only traced ones.

```ts
export default defineConfig({
  traceDeps: [
    "sharp",
    "better-sqlite3",
    "my-pkg*", // full trace (copy all package files)
    "!unwanted-pkg", // exclude from tracing
  ],
});
```

### `traceOpts`

Advanced options passed to [nf3](https://github.com/unjs/nf3) for dependency tracing.

```ts
export default defineConfig({
  traceOpts: {
    // Options passed to @vercel/nft for file tracing
    nft: { /* ... */ },
    // Alias for module paths when tracing
    traceAlias: { "old-pkg": "new-pkg" },
    // Preserve or set file permissions when copying (true or octal like 0o755)
    chmod: true,
    // Transform traced files before copying
    transform: [
      { filter: (id) => id.endsWith(".js"), handler: (code) => code },
    ],
    // Hook into tracing lifecycle
    hooks: {
      tracedPackages(pkgs) {
        console.log("Traced packages:", Object.keys(pkgs));
      },
    },
  },
});
```

### `oxc`

OXC options for the `rollup` builder. Includes `minify` and `transform` sub-options.

`minify` uses the minifier of `rolldown`. If `rolldown` is not installed, minification is skipped with a warning.

```ts
export default defineConfig({
  oxc: {
    minify: { compress: true, mangle: true },
  },
});
```

## Advanced

Low-level options. Most projects should not need to change these.

### `dev`

- Default: `true`{lang=ts} for development and `false`{lang=ts} for production

::warning
This is an advanced configuration. Things can go wrong if misconfigured.
::

```ts
export default defineConfig({
  dev: true, // force development mode behavior
});
```

### `typescript`

- Default: `{}`{lang=ts}

TypeScript options. `tsConfig` is the config used by the bundler for JSX options and path aliases and defaults to the project's `tsconfig.json`. `tsconfigPaths` (default: `true`) resolves its path aliases (`compilerOptions.paths`) for the server sources.

```ts
export default defineConfig({
  typescript: {
    tsConfig: {
      compilerOptions: {
        jsx: "react-jsx",
      },
    },
  },
});
```

### `hooks`

::warning
This is an advanced configuration. Things can go wrong if misconfigured.
::

Nitro hooks. See [hookable](https://github.com/unjs/hookable) for more information.

```ts
export default defineConfig({
  hooks: {
    compiled(nitro) {
      console.log("Build compiled successfully!");
    },
  },
});
```

Hooks with the same name do not override each other. Hooks defined by the deployment preset run first, then hooks from your config. Defining a hook with the same name no longer disables the preset's hook.

:read-more{to="/docs/lifecycle"}

### `commands`

::warning
This is an advanced configuration. Things can go wrong if misconfigured.
::

Preview and deploy command hints are usually filled by deployment presets.

`deploy` can be a shell command (`./` paths are resolved relative to the output directory) or a function receiving the Nitro instance, used by presets that deploy programmatically. Both are run by [`nitro deploy`](/docs/cli#nitro-deploy) and the [`deploy`](/docs/utils#nitrobuilder) builder API.

```ts
export default defineConfig({
  commands: {
    preview: "node ./server/index.mjs",
    deploy: "npx wrangler deploy ./server/index.mjs",
  },
});
```

### `devErrorHandler`

::warning
This is an advanced configuration. Things can go wrong if misconfigured.
::

A custom error handler function for development errors.

```ts
export default defineConfig({
  devErrorHandler: (error, event) => {
    return new Response(`Dev error: ${error.message}`, { status: 500 });
  },
});
```

### `tracingChannel`

**🧪 Experimental!**

- Type: `boolean`{lang=ts} | `{ srvx?: boolean, h3?: boolean, unstorage?: boolean }`{lang=ts}
- Default: `false`{lang=ts}

The `tracingChannel` option enables [`TracingChannel`](https://nodejs.org/api/diagnostics_channel.html#class-tracingchannel) instrumentation for request handling. It is wired for Node.js and also works in Bun, Deno, and the development preset, any runtime that exposes `node:diagnostics_channel`. The published `srvx.request` and `h3.request` channels remain available across these runtimes and can be consumed by APM tools and custom subscribers.

Pass `true` to enable both channels, or an object to toggle them individually.

```ts
export default defineConfig({
  tracingChannel: true,
});
```

### `framework`

- Default: `{ name: "nitro", version: "<current>" }`{lang=ts}

Framework information. Used by presets and build info. Typically set by higher-level frameworks (e.g. Nuxt).

```ts
export default defineConfig({
  framework: { name: "my-framework", version: "2.0.0" },
});
```

### `manifest`

Build manifest options. Use `deploymentId` to include a custom deployment identifier in the build manifest (some presets fill it automatically from platform environment variables).

```ts
export default defineConfig({
  manifest: { deploymentId: process.env.DEPLOY_ID },
});
```

## Preset options

Provider-specific options, applied when the matching [deployment preset](/deploy) is used.

### `awsAmplify`

The options for the AWS Amplify preset. Main fields are `catchAllStaticFallback`, `imageOptimization` (`path`, `cacheControl`), `imageSettings`, and `runtime` (`nodejs20.x` | `nodejs22.x` | `nodejs24.x`, default `nodejs22.x`). See [Preset Docs](/deploy/providers/aws-amplify).

```ts
export default defineConfig({
  awsAmplify: {
    runtime: "nodejs22.x",
    imageOptimization: { path: "/_image", cacheControl: "public, max-age=3600" },
  },
});
```

### `awsLambda`

The options for the AWS Lambda preset. Set `streaming` to `true` to enable response streaming. See [Preset Docs](/deploy/providers/aws).

```ts
export default defineConfig({
  awsLambda: {
    streaming: true,
  },
});
```

### `azure`

The options for the Azure Static Web Apps preset. Use `config` to customize the generated `staticwebapp.config.json` (e.g. `platform.apiRuntime`, `navigationFallback`). See [Preset Docs](/deploy/providers/azure).

```ts
export default defineConfig({
  azure: {
    config: { platform: { apiRuntime: "node:20" } },
  },
});
```

### `firebase`

The options for the Firebase App Hosting preset. Use `appHosting` to customize the generated run configuration. See [Preset Docs](/deploy/providers/firebase#run-configuration).

```ts
export default defineConfig({
  firebase: {
    appHosting: {
      memoryMiB: 512,
      maxInstances: 10,
    },
  },
});
```

### `iis`

The options for the IIS presets. Use `mergeConfig` to merge the generated `web.config` with an existing one, or `overrideConfig` to replace it entirely. See [Preset Docs](/deploy/providers/iis).

```ts
export default defineConfig({
  iis: {
    mergeConfig: true,
  },
});
```

### `netlify`

The options for the Netlify presets. Use `config` to provide Netlify [deploy configuration](https://docs.netlify.com/build/frameworks/frameworks-api/) (`headers`, `redirects`, `functions`, `edge_functions`, `images`, ...). See [Preset Docs](/deploy/providers/netlify).

```ts
export default defineConfig({
  netlify: {
    config: {
      redirects: [{ from: "/old", to: "/new", status: 301 }],
    },
  },
});
```

### `vercel`

The options for the Vercel preset. See [Preset Docs](/deploy/providers/vercel).

```ts
export default defineConfig({
  vercel: {
    config: { runtime: "nodejs20.x" },
  },
});
```

### `cloudflare`

The options for the Cloudflare preset. See [Preset Docs](/deploy/providers/cloudflare).

```ts
export default defineConfig({
  cloudflare: {
    wrangler: { compatibility_date: "2025-01-01" },
  },
});
```

### `zephyr`

The options for the Zephyr preset. See [Preset Docs](/deploy/providers/zephyr#options).

---

### 4.Examples 0.Index (`docs/4.examples/0.index.md`)

---
icon: i-lucide-folder-code
---

# Examples

> Explore Nitro examples to learn how to build full-stack applications

---

### Package.Json (`docs/package.json`)

{
  "private": true,
  "scripts": {
    "dev": "undocs dev",
    "build": "undocs build"
  },
  "devDependencies": {
    "automd": "^0.4.3",
    "motion-v": "^2.8.0",
    "undocs": "npm:undocs-nightly@0.10.1-20261020-132000-e950229"
  }
}

---

### Pnpm Workspace.Yaml (`docs/pnpm-workspace.yaml`)

packages: []
allowBuilds:
  '@parcel/watcher': false
  esbuild: false
  sharp: false
  vue-demi: false
minimumReleaseAgeExclude:
  - undocs-nightly*
  - crossws@0.4.12
  - db0@0.4.0
  - env-runner@0.2.0
  - h3@2.0.1-rc.29
  - nitro-nightly@3.0.1-20260820-224726-7536de7e
  - srvx@0.12.7
  - unstorage@2.0.0-alpha.9

---

### .Config Docs.Yaml (`docs/.config/docs.yaml`)

# yaml-language-server: $schema=https://unpkg.com/undocs/schema/config.json
name: Nitro
shortDescription: builds full-stack servers that deploy anywhere
description: Add server API routes to any Vite apps and deploy with zero configuration on your favorite hosting platform.
github: nitrojs/nitro
url: https://nitro.build
themeColor: "pink"
automd: true
branch: main
versions:
  - label: "v3"
    active: true
  - label: "v2"
    to: "https://v2.nitro.build"
socials:
  x: "https://x.com/nitrojsdev"
  bluesky: "https://bsky.app/profile/nitro.build"
  discord: https://discord.nitro.build
sponsors:
  api: https://sponsors.pi0.io/sponsors.json
redirects:
  # v2 guide → v3 docs
  "/guide": "/docs"
  "/guide/getting-started": "/docs"
  "/guide/utils": "/docs/routing"
  "/guide/routing": "/docs/routing"
  "/guide/websocket": "/docs/websocket"
  "/guide/cache": "/docs/cache"
  "/guide/storage": "/docs/storage"
  "/guide/database": "/docs/database"
  "/guide/fetch": "/docs/routing"
  "/guide/assets": "/docs/assets"
  "/guide/plugins": "/docs/plugins"
  "/guide/configuration": "/docs/configuration"
  "/guide/typescript": "/docs/configuration"
  "/guide/tasks": "/docs/tasks"
  "/guide/nightly": "/docs/nightly"
  "/guide/nightly-channel": "/docs/nightly"
  "/guide/migration": "/docs/migration"
  "/guide/community/contributing": "/docs"
  # v2 deploy
  "/deploy/workers": "/deploy"
  "/deploy/runtimes/winterjs": "/deploy"
  "/deploy/custom-presets": "/deploy"
  "/deploy/providers/deno-deploy": "/deploy/runtimes/deno#deno-deploy"
  "/deploy/providers/edgio": "/deploy"
  "/deploy/providers/lagon": "/deploy"
  "/deploy/providers/layer0": "/deploy"
  "/deploy/providers/edgeone-pages": "/deploy/providers/edgeone"
  "/deploy/providers/platform-sh": "/deploy/providers/upsun"
  "/deploy/node": "/deploy/runtimes/node"

---

### 1.Docs 2.Quick Start (`docs/1.docs/2.quick-start.md`)

---
icon: i-lucide-zap
---

# Quick Start

> Start with a fresh Nitro project or adopt it in your current Vite project.

## Try Nitro online

Get a taste of Nitro in your browser using our playground.

[Play with Nitro in StackBlitz](https://stackblitz.com/github/nitrojs/starter/tree/v3-vite?file=index.html,server.ts){target="_blank"}

## Create a Nitro project

The fastest way to create a Nitro application is with `create-nitro-app`.

> [!NOTE]
> Make sure you have the latest LTS version of [Node.js](https://nodejs.org/en), [Bun](https://bun.sh/), or [Deno](https://deno.com/) installed.

:pm-x{command="create-nitro-app"}

<details>
  <summary>Preview</summary>
  <div style="display:flex;justify-content:center;">
    <img src="https://github.com/nitrojs/create-nitro-app/blob/main/.images/preview-dark.svg?raw=true" alt="Preview" style="max-width:100%;height:auto;display:block;" />
  </div>
</details>

Follow the CLI instructions and you will be ready to start your development server.



## Add to a Vite project

You can add Nitro to any existing Vite project to get API routes, server-side rendering, and more.

::steps{level="3"}

### Install `nitro` and `vite`

:pm-install{name="nitro vite"}

### Add Nitro plugin to Vite

Add the Nitro plugin to your `vite.config.ts`:

```ts [vite.config.ts] {2,6}
import { defineConfig } from "vite";
import { nitro } from "nitro/vite";

export default defineConfig({
  plugins: [
    nitro()
  ],
});
```

### Configure Nitro

Create a `nitro.config.ts` to configure the server directory:

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

export default defineConfig({
  serverDir: "./server",
});
```

The `serverDir` option tells Nitro where to look for your server routes. In this example, all routes will be inside the `server/` directory. By default `serverDir` is `false`, so no directories are scanned until you set it (or `scanDirs`).

### Create an API route

Create your first API route at `server/api/test.ts`:

::code-tree{defaultValue="server/api/test.ts"}
```ts [server/api/test.ts]
import { defineHandler } from "nitro";

export default defineHandler(() => {
  return { message: "Hello Nitro!" };
});
```
```ts [nitro.config.ts]
import { defineConfig } from "nitro";

export default defineConfig({
  serverDir: "./server",
});
```
```ts [vite.config.ts]
import { defineConfig } from "vite";
import { nitro } from "nitro/vite";

export default defineConfig({
  plugins: [nitro()],
});
```
::

The file path maps directly to the route URL: `server/api/test.ts` becomes `/api/test`. Handlers can return strings, JSON objects, `Response` instances, or readable streams.

::tip
As an alternative to filesystem routing, you can declare routes programmatically using the `routes` config option. See [Programmatic route handlers](/docs/routing#programmatic-route-handlers) for more details.
::

### Start the development server

:pm-run{script="dev"}

Your API route is now accessible at `http://localhost:3000/api/test` :sparkles:

::

## Next steps

- Learn about dynamic routes, methods, and middleware in [Routing](/docs/routing).
- Serve HTML or a single-page app for unmatched routes with the [renderer](/docs/renderer).
- Take full control of every request with a [server entry](/docs/server-entry).

---

### 1.Docs 4.Renderer (`docs/1.docs/4.renderer.md`)

---
icon: ri:layout-masonry-line
navigation:
  title: Renderer
---

# Nitro Renderer

> Use a renderer to handle all unmatched routes with custom HTML or a templating system.

The renderer is a special handler in Nitro that catches all routes that don't match any specific API or route handler. It's commonly used for server-side rendering (SSR), serving single-page applications (SPAs), or creating custom HTML responses.

## Configuration

The renderer is configured using the `renderer` option in your Nitro config:

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

export default defineConfig({
  renderer: {
    template: './index.html',  // Path to HTML template file
    handler: './renderer.ts',  // Path to custom renderer handler
    static: false,             // Force dynamic template processing (auto-detected when unset)
  }
})
```

| Option | Type | Description |
| --- | --- | --- |
| `template` | `string` | Path to an HTML file used as the renderer template. |
| `handler` | `string` | Path to a custom renderer handler module. |
| `static` | `boolean` | When `true`, skips rendu template processing and serves the HTML as-is. Auto-detected based on template syntax when not set. |

Set `renderer: false` in the config to explicitly disable the renderer entirely (including auto-detection of `index.html`).

## HTML template

### Auto-detected `index.html`

By default, Nitro automatically looks for an `index.html` file in your project root or one of the scanned directories.

If found, Nitro will use it as the renderer template and serve it for all unmatched routes.

::code-group
```html [index.html]
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>My Vite + Nitro App</title>
  </head>
  <body>
    <div id="app"></div>
    <script type="module" src="/src/main.ts"></script>
  </body>
</html>
```
```ts [routes/api/hello.ts]
import { defineHandler } from "nitro";

export default defineHandler((event) => {
  return { hello: "API" };
});
```
::

::tip
When `index.html` is detected, Nitro will automatically log in the terminal: `Using index.html as renderer template.`
::

With this setup:
- `/api/hello` → Handled by your API routes
- `/about`, `/contact`, etc. → Served with `index.html`

### Custom HTML file

You can specify a custom HTML template file using the `renderer.template` option in your Nitro configuration.

::code-group
```ts [nitro.config.ts]
import { defineConfig } from "nitro";

export default defineConfig({
  renderer: {
    template: './app.html'
  }
})
```

```html [app.html]
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Custom Template</title>
  </head>
  <body>
    <div id="root">Loading...</div>
    <script type="module" src="/src/main.js"></script>
  </body>
</html>
```
::

### Static templates

By default, Nitro auto-detects whether your HTML template contains [rendu](#hypertext-preprocessor-experimental) syntax. If it does, the template is processed dynamically on each request. If it doesn't, it's served as static HTML.

You can override this behavior with the `static` option:

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

export default defineConfig({
  renderer: {
    template: './index.html',
    static: true // Force static serving, skip template processing
  }
})
```

In production, static templates are inlined into the server bundle and served directly for optimal performance.

### Hypertext Preprocessor (experimental)

For dynamic templates, Nitro uses the [rendu](https://github.com/h3js/rendu) Hypertext Preprocessor, a simple and powerful way to create dynamic HTML templates with JavaScript expressions.

#### Output expressions

- `{{ expression }}`: HTML-escaped output
- `{{{ expression }}}` or `<?= expression ?>`: raw (unescaped) output

```html
<h1>Hello {{ $URL.pathname }}</h1>
<div>{{{ '<strong>raw html</strong>' }}}</div>
```

#### Control flow

Use `<? ... ?>` for JavaScript control flow:

```html
<? if ($METHOD === 'POST') { ?>
  <p>Form submitted!</p>
<? } else { ?>
  <form method="POST">
    <button type="submit">Submit</button>
  </form>
<? } ?>

<ul>
<? for (const item of ['a', 'b', 'c']) { ?>
  <li>{{ item }}</li>
<? } ?>
</ul>
```

#### Server scripts

Use `<script server>` to execute JavaScript on the server:

```html
<script server>
  const data = await fetch('https://api.example.com/data').then(r => r.json());
</script>
<pre>{{ JSON.stringify(data) }}</pre>
```

#### Streaming content

Use the `echo()` function for streaming content. It accepts strings, functions, Promises, Response objects, or ReadableStreams:

```html
<script server>
  echo("Loading...");
  echo(async () => fetch("https://api.example.com/data"));
</script>
```

#### Global variables

Access request context within templates:

| Variable | Description |
| --- | --- |
| `$REQUEST` | The incoming `Request` object |
| `$METHOD` | HTTP method (`GET`, `POST`, etc.) |
| `$URL` | Request `URL` object |
| `$HEADERS` | Request headers |
| `$RESPONSE` | Response configuration object |
| `$COOKIES` | Read-only object containing request cookies |

#### Built-in functions

| Function | Description |
| --- | --- |
| `htmlspecialchars(str)` | Escape HTML characters (automatically applied in `{{ }}` syntax) |
| `setCookie(name, value, options?)` | Set a cookie in the response |
| `redirect(url)` | Redirect the user to another URL |
| `echo(content)` | Stream content to the response |

```html [index.html]
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Dynamic template</title>
  </head>
  <body>
    <h1>Hello {{ $REQUEST.url }}</h1>
    <p>Welcome, <?= $COOKIES["user"] || "Guest" ?>!</p>
    <script server>
      setCookie("visited", "true", { maxAge: 3600 });
    </script>
  </body>
</html>
```

:read-more{to="https://github.com/h3js/rendu" title="Rendu Documentation"}

## Custom renderer handler

For more complex scenarios, you can create a custom renderer handler that programmatically generates responses.

The handler is a default export function that receives an H3 event object. You can access the incoming `Request` via `event.req`:

```ts [renderer.ts]
export default function renderer({ req }: { req: Request }) {
  const url = new URL(req.url);
  return new Response(
    /* html */ `<!DOCTYPE html>
    <html>
    <head>
      <title>Custom Renderer</title>
    </head>
    <body>
      <h1>Hello from custom renderer!</h1>
      <p>Current path: ${url.pathname}</p>
    </body>
    </html>`,
    { headers: { "content-type": "text/html; charset=utf-8" } }
  );
}
```

Then, specify the renderer entry in the Nitro config:

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

export default defineConfig({
  renderer: {
    handler: './renderer.ts'
  }
})
```

::note
When `renderer.handler` is set, it takes full control of rendering. The `renderer.template` option is ignored.
::

## Renderer priority

The renderer always acts as a catch-all route (`/**`) and has the **lowest priority**. This means:

1. Specific API routes are matched first (e.g., `/api/users`)
2. Specific server routes are matched next (e.g., `/about`)
3. The renderer catches everything else

```md
api/
  users.ts        → /api/users (matched first)
routes/
  about.ts        → /about (matched second)
renderer.ts         → /** (catches all other routes)
```

::note
If you define your own catch-all route (`[...].ts`), it is chained with the other `/**` handlers rather than replaced: catch-all handlers run in registration order (file-based catch-all, then [server entry](/docs/server-entry), then renderer), and the first one to return a response wins. The renderer only handles requests your catch-all leaves unanswered.
::

:read-more{to="/docs/lifecycle" title="Lifecycle"}

## Vite integration

When using Nitro with Vite, the renderer integrates with Vite's build pipeline and dev server.

### Development mode

In development, the renderer template is read from disk on each request, so changes to `index.html` are reflected immediately without restarting the server. Vite's `transformIndexHtml` hook is applied to inject HMR client scripts and other dev-time transforms.

### SSR with ``

When using Vite environments with an `ssr` service, you can add an `` comment to your `index.html`. Nitro will replace it with the output from your SSR entry during rendering:

```html [index.html]
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>SSR App</title>
  </head>
  <body>
    <div id="app"></div>
    <script type="module" src="/src/main.ts"></script>
  </body>
</html>
```

### Production build

During production builds, Vite processes the `index.html` through its build pipeline (resolving scripts, CSS, and other assets), then Nitro inlines the transformed HTML into the server bundle.

## Use cases

### Single-page application (SPA)

Serve your SPA's `index.html` for all routes to enable client-side routing:

> [!TIP]
> This is the default behavior of Nitro when used with Vite.

---

### 1.Docs 5.Routing (`docs/1.docs/5.routing.md`)

---
icon: ri:direction-line
---

# Routing

> Nitro supports filesystem routing to automatically map files to routes. Routes are compiled and code-split at build time, removing the need for a runtime router.

## Route handlers

A route handler is a function that receives an `event` object ([H3Event](https://h3.dev/guide/api/h3event#h3event-properties)) and returns a response.

::code-group
```ts [defineHandler]
import { defineHandler } from "nitro";

export default defineHandler((event) => {
  return "world";
});
```
```ts [Single function]
import type { H3Event } from "nitro";

export default (event: H3Event) => {
  return "world";
};
```
::

## Filesystem routing

Files are automatically mapped to [h3 routes](https://h3.dev/guide/basics/routing). Defining a route is as simple as creating a file inside the `api/` or `routes/` directory of your [`serverDir`](/docs/configuration).

::note
Filesystem routing requires setting [`serverDir`](/docs/configuration) (or `scanDirs`) in your config; no directories are scanned by default.
::

Each file defines a single handler, and you can [append the HTTP method](#specific-request-method) to the filename to match a specific request method.

```
routes/
  api/
    test.ts      <-- /api/test
  hello.get.ts   <-- /hello (GET only)
  hello.post.ts  <-- /hello (POST only)
vite.config.ts
```

You can nest routes by creating subdirectories.

```txt
routes/
  api/
    [org]/
      [repo]/
        index.ts   <-- /api/:org/:repo
        issues.ts  <-- /api/:org/:repo/issues
      index.ts     <-- /api/:org
package.json
```

### Route groups

To organize related route files without affecting the resulting URLs, put them in a folder wrapped in parentheses `(` and `)`.

For example:
```txt
routes/
  api/
    (admin)/
      users.ts   <-- /api/users
      reports.ts <-- /api/reports
    (public)/
      index.ts   <-- /api
package.json
```
> [!NOTE]
> Route groups are not part of the route path and are only used for organization purposes.

### Static routes

Create a file in the `routes/` or `routes/api/` directory. The file path becomes the route path. Then export a route handler:

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

export default defineHandler(() => {
  return { hello: "API" };
});
```

### Dynamic routes

#### Single param

To define a route with params, use the `[<param>]` syntax where `<param>` is the name of the param. The param will be available on the `event.context.params` object.

```ts [routes/hello/[name\\].ts]
import { defineHandler } from "nitro";

export default defineHandler((event) => {
  const { name } = event.context.params;

  return `Hello ${name}!`;
});
```

Calling `/hello/nitro` returns:

```txt [Response]
Hello nitro!
```

#### Multiple params

You can define multiple params in a route using `[<param1>]/[<param2>]` syntax where each param is a folder. You **cannot** define multiple params in a single filename or folder.

```ts [routes/hello/[name\\]/[age\\].ts]
import { defineHandler } from "nitro";

export default defineHandler((event) => {
  const { name, age } = event.context.params;

  return `Hello ${name}! You are ${age} years old.`;
});
```

#### Catch-all params

You can capture all the remaining parts of a URL using `[...<param>]` syntax. This will include the `/` in the param.

```ts [routes/hello/[...name\\].ts]
import { defineHandler } from "nitro";

export default defineHandler((event) => {
  const { name } = event.context.params;

  return `Hello ${name}!`;
});
```

Calling `/hello/nitro/is/hot` returns:

```txt [Response]
Hello nitro/is/hot!
```

A catch-all param also matches the base path: `/hello` and `/hello/` are handled by `routes/hello/[...name].ts` too, with `name` being `undefined` (for `/hello`) or `""` (for `/hello/`). A sibling `routes/hello/index.ts` takes precedence for the base path. A root `routes/[...name].ts` also matches `/`, with `name` being `""`.

When catch-alls are nested, the innermost one handles its own base path. With `routes/[...slug].ts` and `routes/docs/[...path].ts`, `/docs` is handled by `docs/[...path].ts` (with `path` undefined), not by the root catch-all.

### Specific request method

You can append the HTTP method to the filename to force the route to be matched only for a specific HTTP request method, for example `hello.get.ts` will only match for `GET` requests. You can use any HTTP method you want.

Supported methods: `get`, `post`, `put`, `delete`, `patch`, `head`, `options`, `query`, `connect`, `trace`.

If a `.head` route matches, it handles `HEAD` requests. Otherwise they are handled by the route that would handle a `GET` request (including a `.get` route over a method-agnostic one, and `GET`-scoped middleware). The response body is always dropped for `HEAD` requests, including those handled by a `.head` route.

::note
`query` matches the [`QUERY` method](https://datatracker.ietf.org/doc/draft-ietf-httpbis-safe-method-w-body/), a safe and idempotent alternative to `GET` that carries a request body. Support across proxies, CDNs and deployment platforms is still uneven, so keep a `GET` or `POST` fallback for public APIs.
::

When a request path matches a route only for other methods, Nitro responds with `405 Method Not Allowed` and an `Allow` header listing them. If a catch-all route (such as `routes/[...].ts`, a renderer or a server entry) matches the request, it handles it instead.

::code-group
```ts [GET]
// routes/users/[id].get.ts
import { defineHandler } from "nitro";

export default defineHandler(async (event) => {
  const { id } = event.context.params;

  // Do something with id

  return `User profile!`;
});
```

```ts [POST]
// routes/users.post.ts
import { defineHandler } from "nitro";

export default defineHandler(async (event) => {
  const body = await event.req.json();

  // Do something with body like saving it to a database

  return { updated: true };
});
```
::

### Catch-all route

You can create a special route that matches all requests not handled by any other route, useful as a default or fallback route.

To create a catch-all route, create a file named `[...].ts`.

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

export default defineHandler((event) => {
  return `Hello ${event.url.pathname}!`;
});
```

::note
With an unnamed catch-all (`[...].ts`), the matched wildcard segments are available as `event.context.params[0]`.
::

The [server entry](/docs/server-entry) and [renderer](/docs/renderer) also act as catch-all handlers and are chained after file-based catch-all routes.

### Environment-specific handlers

You can include a route only in specific builds by adding a `.dev`, `.prod`, or `.prerender` suffix to the filename, for example: `routes/test.get.dev.ts` or `routes/test.get.prod.ts`.

The suffix is placed after the method suffix (if any):

```txt
routes/
  env/
    index.dev.ts       <-- /env (dev only)
    index.get.prod.ts  <-- /env (GET, prod only)
```

> [!TIP]
> To target multiple environments, or a preset name as the environment, register the route programmatically via the [`routes`](#routes-config) config.

### Reserved suffixes

Nitro reserves trailing lowercase suffixes in the names of scanned files (`routes/`, `api/`, `middleware/`, `plugins/`, `tasks/` and `modules/`) for its own conventions. Known file extensions are kept as part of the route path:

```txt
routes/
  sitemap.xml.ts     <-- /sitemap.xml
  docs/[slug].md.ts  <-- /docs/:slug.md
  hello.world.ts     <-- error: unknown suffix `.world`
```

Suffixes with digits, uppercase letters or other characters (e.g. `v1.2.ts`) are always kept.

To keep a custom suffix, add it to the [`scanSuffixes`](/config#scansuffixes) config:

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

export default defineConfig({
  scanSuffixes: ["world"], // routes/hello.world.ts -> /hello.world
});
```

### Ignoring files

Test files (`*.test.ts`, `*.spec.ts`) and type declarations (`*.d.ts`) are never registered, so they can live next to your handlers.


You can use the `ignore` config option to exclude files from route scanning. It accepts an array of glob patterns relative to the server directory.

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

export default defineConfig({
  ignore: [
    "routes/api/**/_*",   // Ignore files starting with _ in api/
    "middleware/_*.ts",    // Ignore middleware starting with _
    "routes/_*.ts",       // Ignore root routes starting with _
  ],
});
```

### Route meta

You can define route metadata at build time using the `defineRouteMeta` macro in your route handler files.

> [!IMPORTANT]
> This feature is currently experimental.

```ts [routes/api/test.ts]
import { defineRouteMeta } from "nitro";
import { defineHandler } from "nitro";

defineRouteMeta({
  openAPI: {
    tags: ["test"],
    description: "Test route description",
    parameters: [{ in: "query", name: "test", required: true }],
  },
});

export default defineHandler(() => "OK");
```

::read-more{to="https://swagger.io/specification/v3/"}
This feature is currently usable to specify OpenAPI meta. See the Swagger specification for available OpenAPI options.
::

## Programmatic route handlers

In addition to filesystem routing, you can register route handlers programmatically using the `routes` config option.

### `routes` config

The `routes` option allows you to map route patterns to handlers:

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

export default defineConfig({
  routes: {
    "/api/hello": "./server/routes/api/hello.ts",
    "/api/custom": {
      handler: "./server/routes/api/hello.ts",
      method: "POST",
      lazy: true,
    },
    "/virtual": {
      handler: "#virtual-route",
    },
  },
});
```

Each route entry can be a simple string (handler path) or an object with the following options:

| Option | Type | Description |
|--------|------|-------------|
| `handler` | `string` | Path to event handler file or virtual module ID |
| `method` | `string` | HTTP method to match (`get`, `post`, etc.) |
| `lazy` | `boolean` | Use lazy loading to import handler |
| `format` | `"web" \| "node"` | Handler type. `"node"` handlers are converted to web-compatible |
| `env` | `string \| string[]` | Environments to include this handler (`"dev"`, `"prod"`, `"prerender"`, or a preset name) |

### `handlers` config

The `handlers` array is useful for registering middleware with control over route matching:

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

export default defineConfig({
  handlers: [
    {
      route: "/api/**",
      handler: "./server/utils/api-auth.ts",
      middleware: true,
    },
  ],
});
```

Each handler entry supports the following options:

| Option | Type | Description |
|--------|------|-------------|
| `route` | `string` | HTTP pathname pattern (e.g., `/test`, `/api/:id`, `/blog/**`) |
| `handler` | `string` | Path to event handler file or virtual module ID |
| `method` | `string` | HTTP method to match (`get`, `post`, etc.) |
| `middleware` | `boolean` | Run handler as middleware before route handlers |
| `lazy` | `boolean` | Use lazy loading to import handler |
| `format` | `"web" \| "node"` | Handler type. `"node"` handlers are converted to web-compatible |
| `env` | `string \| string[]` | Environments to include this handler (`"dev"`, `"prod"`, `"prerender"`, or a preset name) |

## Middleware

Middleware run before route handlers, letting you inspect or extend the incoming request. See [Lifecycle](/docs/lifecycle) for where middleware fit in the request pipeline.

::note
Middleware can modify the request before it is processed, not after.
::

Middleware are auto-registered from the `middleware/` directory.

```md
middleware/
  auth.ts
  logger.ts
  ...
routes/
  hello.ts
```

### Simple middleware

Middleware are defined exactly like route handlers, with one exception: they should not return anything.

```ts [middleware/auth.ts]
import { defineHandler } from "nitro";

export default defineHandler((event) => {
  // Extend or modify the event
  event.context.user = { name: "Nitro" };
});
```

Middleware in the `middleware/` directory are automatically registered for all routes. To register middleware for a specific route pattern, see [Route-scoped middleware](#route-scoped-middleware).

::warning
Returning a value from middleware closes the request: the value becomes the response and no further handlers run. Avoid returning from middleware; use a route handler instead.
::

### Execution order

Middleware are executed in directory listing order.

```md
middleware/
  auth.ts <-- First
  logger.ts <-- Second
  ... <-- Third
```

Prefix middleware with a number to control their execution order.

```md
middleware/
  1.logger.ts <-- First
  2.auth.ts <-- Second
  3.... <-- Third
```
::note
File names are sorted as strings, so `10.filename.ts` comes right after `1.filename.ts` and before `2.filename.ts`. If you have more than 10 middleware in the same directory, prefix `1-9` with a `0` (e.g. `01.filename.ts`).
::

### Request filtering

Global middleware run on every request, but you can add your own conditions to scope them. For example, check the URL to apply a middleware to a specific route:

```ts [middleware/auth.ts]
import { defineHandler } from "nitro";

export default defineHandler((event) => {
  // Will only execute for /auth route
  if (event.url.pathname.startsWith('/auth')) {
    event.context.user = { name: "Nitro" };
  }
});
```

### Route-scoped middleware

You can register middleware for specific route patterns using the [`handlers`](#handlers-config) config with the `middleware` option and a specific `route`:

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

export default defineConfig({
  handlers: [
    {
      route: "/api/**",
      handler: "./server/utils/api-auth.ts",
      middleware: true,
    },
  ],
});
```

Unlike global middleware (auto-registered from the `middleware/` directory, matching `/**`), route-scoped middleware only run for requests matching the specified pattern.

Keep route-scoped handler files outside `middleware/` (as in the example above) so directory scanning does not also register them globally.

## Error handling

You can use the [utilities available in H3](https://h3.dev/guide/basics/error) to handle errors in both route handlers and middleware.

The way errors are sent back to the client depends on the environment. In development, requests with an `Accept` header of `text/html` (such as browsers) receive an HTML error page. In production, errors are always sent as JSON. This behavior can be influenced by request properties such as the `Accept` or `User-Agent` headers.

To capture errors globally with the `error` hook, see [Lifecycle](/docs/lifecycle#error-handling).

## Code splitting

Nitro creates a separate chunk for each route handler. Chunks load on-demand when first requested, so `/api/users` doesn't load code for `/api/posts`.

See [`inlineDynamicImports`](/config#inlinedynamicimports) to bundle everything into a single file.

## Route rules

Route rules let you attach behavior (redirects, proxying, caching, authentication, custom headers) to route patterns directly from your configuration, without writing handler code.

Each rule maps a route pattern (using [rou3](https://github.com/h3js/rou3) syntax) to a set of options, built on [h3 route rules](https://h3.dev/guide/rules). When the `cache` option is set, matching handlers are automatically wrapped with `defineCachedHandler`. See the [cache guide](/docs/cache) to learn more.

Set route rules with the `routeRules` config option:

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

export default defineConfig({
  routeRules: {
    '/blog/**': { swr: true },
    '/blog2/**': { swr: 600 },
    '/blog3/**': { static: true },
    '/blog4/**': { cache: { /* cache options*/ } },
    '/assets/**': { headers: { 'cache-control': 's-maxage=0' } },
    '/api/v1/**': { cors: true, headers: { 'access-control-allow-methods': 'GET' } },
    '/old-page': { redirect: '/new-page' },
    '/old-page/**': { redirect: '/new-page/**' },
    '/proxy/example': { proxy: 'https://example.com' },
    '/proxy/**': { proxy: '/api/**' },
  }
});
```

::note
Netlify and Cloudflare `_headers` and `_redirects` only support static segments, `:name` params and a single `*` splat (as the last segment in `_redirects`). Rules with other patterns, such as `/num/:id(\d+)` or `/a/:id?`, are left out of them (with a build warning) and only the runtime applies them, which means static assets don't get their headers. A `redirect` rule that also matches the requests of a skipped one, such as `/num/**` next to `/num/:id(\d+)`, is left out of `_redirects` too, so that it cannot take precedence over it.
::

### Rule merging and overrides

Route rules are matched from least specific to most specific. When multiple rules match a request, their options are merged, with more specific rules taking precedence.

You can use `false` to disable a rule that was set by a more general pattern:

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

export default defineConfig({
  routeRules: {
    '/api/cached/**': { swr: true },
    '/api/cached/no-cache': { cache: false, swr: false },
    '/api/**': { cors: true },
    '/api/internal/**': { cors: false },
  }
});
```

### Method-scoped rules

Prefix a rule key with an uppercase HTTP method (followed by a space) to scope it to requests using that method. Keys without a method prefix apply to every method. A method-scoped rule is merged on top of the method-agnostic rules that match the same path, so you can layer method-specific behavior over shared defaults:

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

export default defineConfig({
  routeRules: {
    // Applies to every method
    '/api/**': { headers: { 'x-api': 'true' } },
    // Only POST requests to /api/** get this header
    'POST /api/**': { headers: { 'x-write': 'true' } },
    // Cache GET requests to /feed only
    'GET /feed': { swr: 600 },
  }
});
```

::note
Method matching is resolved per request by the server runtime. Vercel and EdgeOne `config.json` routes are scoped to the same methods. Netlify and Cloudflare `_headers` and `_redirects` cannot match by method, so method-scoped rules are left out of them (with a build warning) and only the runtime applies them, which means static assets don't get their headers. A method-agnostic `redirect` rule that also matches the requests of a skipped method-scoped one, such as `/old/**` next to `GET /old/**`, is left out of `_redirects` too, so that it cannot take precedence over it. Prefer method-agnostic keys for `headers` and `redirect` rules that a platform should apply at its edge.
::

### Headers

Set custom response headers for matching routes:

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

export default defineConfig({
  routeRules: {
    '/api/**': { headers: { 'cache-control': 's-maxage=60' } },
    '**': { headers: { 'x-powered-by': 'Nitro' } },
  }
});
```

### CORS

Handle CORS at runtime with the `cors` rule. `cors: true` applies permissive defaults (origin, methods, and allowed headers `*`): a simple request gets `access-control-allow-origin: *` and `access-control-expose-headers: *`, and an `OPTIONS` preflight is answered directly (`204`) with the matching `access-control-allow-*` headers.

> [!NOTE]
> CORS is applied by the running server (h3's [`handleCors`](https://h3.dev/utils/security#handlecorsevent-options)), and not from the static/CDN config. 
> On platforms that can serve prerendered/static assets straight from the edge, CORS headers are only added when the request reaches the server handler.

Pass an object for finer control: an origin allowlist, `credentials`, `maxAge`, etc. (h3 `CorsOptions`). Combining `credentials: true` with a wildcard origin is invalid and throws at build time:

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

export default defineConfig({
  routeRules: {
    // Permissive defaults
    '/api/public/**': { cors: true },
    // Restrict to specific origins with credentials
    '/api/v1/**': {
      cors: { origin: ['https://app.example.com'], credentials: true },
    },
  }
});
```

### Redirect

Redirect matching routes to another URL. Use a string for a simple redirect (defaults to `307` status), or an object for more control:

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

export default defineConfig({
  routeRules: {
    // Simple redirect (307 status)
    '/old-page': { redirect: '/new-page' },
    // Redirect with custom status
    '/legacy': { redirect: { to: 'https://example.com/', status: 308 } },
    // Wildcard redirect, preserves the path after the pattern
    '/old-blog/**': { redirect: 'https://blog.example.com/**' },
  }
});
```

The redirect target is used as written. With a non-root [`baseURL`](/config#baseurl), route rule keys are matched relative to it, but relative targets are not prefixed, so include the `baseURL` in the target when needed.

### Proxy

Proxy requests to another URL. Supports both internal and external targets:

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

export default defineConfig({
  routeRules: {
    // Proxy to exact URL
    '/api/proxy/example': { proxy: 'https://example.com' },
    // Proxy to internal route
    '/api/proxy/**': { proxy: '/api/echo' },
    // Wildcard proxy, preserves the path after the pattern
    '/cdn/**': { proxy: 'https://cdn.jsdelivr.net/**' },
    // Proxy with options
    '/external/**': {
      proxy: {
        to: 'https://api.example.com/**',
        // Additional H3 proxy options...
      },
    },
  }
});
```

### Authentication

There is no `auth` route rule: authentication needs executable logic, so it belongs in [middleware](#middleware). Use h3's [`basicAuth`](https://h3.dev/utils/security#basicauthopts) middleware for a single route:

```ts [routes/admin/index.ts]
import { defineHandler } from "nitro";
import { basicAuth } from "nitro/h3";

export default defineHandler({
  middleware: [basicAuth({ username: 'admin', password: 'supersecret', realm: 'Admin Area' })],
  handler: (event) => `Hello, ${event.context.basicAuth?.username}!`,
});
```

For a whole subtree, register it as [route-scoped middleware](#route-scoped-middleware), or as global middleware in the `middleware/` directory.

### Caching (SWR / Static)

Control caching behavior with `cache`, `swr`, or `static` options:

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

export default defineConfig({
  routeRules: {
    // Enable stale-while-revalidate caching
    '/blog/**': { swr: true },
    // SWR with maxAge in seconds
    '/blog/posts/**': { swr: 600 },
    // Full cache options
    '/api/data/**': {
      cache: {
        maxAge: 60,
        swr: true,
        // ...other cache options
      },
    },
    // Disable caching
    '/api/realtime/**': { cache: false },
  }
});
```

::tip
`swr: true` is a shortcut for `cache: { swr: true }` and `swr: <number>` is a shortcut for `cache: { swr: true, maxAge: <number> }`.
::

### Prerender

Mark routes for prerendering at build time:

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

export default defineConfig({
  routeRules: {
    '/about': { prerender: true },
    '/dynamic/**': { prerender: false },
  }
});
```

Prerendered routes are generated once at build time and served as static files. They are not revalidated at runtime.

::warning
Do not combine `prerender` and `isr` on the same route. Prerendering takes precedence, so `isr` is ignored and the page will not be regenerated after deployment. Use `isr` alone when you need incremental regeneration on supported platforms.
::

### ISR (Vercel)

Configure Incremental Static Regeneration for Vercel deployments:

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

export default defineConfig({
  routeRules: {
    '/isr/**': { isr: true },
    '/isr-ttl/**': { isr: 60 },
    '/isr-custom/**': {
      isr: {
        expiration: 60,
        allowQuery: ['q'],
        group: 1,
      },
    },
  }
});
```

::note
ISR generates pages on demand and revalidates them according to the expiration you set. It is an alternative to build-time `prerender`, not something you combine with it on the same route.
::

### Route rules reference

| Option | Type | Description |
|--------|------|-------------|
| `headers` | `Record<string, string>` | Custom response headers |
| `redirect` | `string \| { to: string, status?: number }` | Redirect to another URL (default status: `307`) |
| `proxy` | `string \| { to: string, ...proxyOptions }` | Proxy requests to another URL |
| `cors` | `boolean \| CorsOptions` | Handle CORS via h3's [`handleCors`](https://h3.dev/utils/security#handlecorsevent-options) (`true` = permissive) |
| `cache` | `object \| false` | Cache options (see [cache guide](/docs/cache)) |
| `swr` | `boolean \| number` | Shortcut for `cache: { swr: true, maxAge: number }` (`false` resets an inherited cache rule) |
| `static` | `boolean \| number` | Shortcut for static caching |
| `prerender` | `boolean` | Enable/disable prerendering |
| `isr` | `boolean \| number \| object` | Incremental Static Regeneration (Vercel) |

### Runtime route rules

Route rules can be provided through `runtimeConfig`, allowing overrides via environment variables without rebuilding:

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

export default defineConfig({
  runtimeConfig: {
    nitro: {
      routeRules: {
        '/api/**': { headers: { 'x-env': 'production' } },
      },
    },
  },
});
```

## Config reference

These config options control routing behavior:

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `baseURL` | `string` | `"/"` | Base URL for all routes |
| `apiBaseURL` | `string` | `"/api"` | Base URL for routes in the `api/` directory |
| `apiDir` | `string` | `"api"` | Directory name for API routes |
| `routesDir` | `string` | `"routes"` | Directory name for file-based routes |
| `serverDir` | `string \| false` | `false` | Server directory for scanning routes, middleware, plugins, etc. |
| `scanDirs` | `string[]` | `[]` | Additional directories to scan for routes |
| `routes` | `Record<string, string \| handler>` | `{}` | Route-to-handler mapping |
| `handlers` | `NitroEventHandler[]` | `[]` | Programmatic handler registration (mainly for middleware) |
| `routeRules` | `Record<string, RouteRuleConfig>` | `{}` | Route rules for matching patterns |
| `ignore` | `string[]` | `[]` | Glob patterns to ignore during file scanning |

---

### 1.Docs 50.Assets (`docs/1.docs/50.assets.md`)

---
icon: ri:image-2-line
---

# Assets

> Serve static files to clients and bundle files into your server for runtime access.

Nitro supports three types of assets: **public assets** served directly to clients, **imported files** inlined into the server bundle, and **server assets** bundled into the server for programmatic access.

## Public Assets

Nitro handles assets via the `public/` directory.

All assets in `public/` directory will be automatically served. This means that you can access them directly from the browser without any special configuration.

```md
public/
  image.png     <-- /image.png
  video.mp4     <-- /video.mp4
  robots.txt    <-- /robots.txt
```

### Caching and Headers

Public assets are served with automatic `ETag` and `Last-Modified` headers for conditional requests. When the client sends `If-None-Match` or `If-Modified-Since` headers, Nitro returns a `304 Not Modified` response.

For assets served from a non-root `baseURL` (such as `/build/`), Nitro prevents fallthrough to application handlers. If a request matches a public asset base but the file is not found, a `404` is returned immediately.

### Production Public Assets

When building your Nitro app, the `public/` directory will be copied to `.output/public/` and a manifest with metadata will be created and embedded in the server bundle.

```json
{
  "/image.png": {
    "type": "image/png",
    "etag": "\"4a0c-6utWq0Kbk5OqDmksYCa9XV8irnM\"",
    "mtime": "2023-03-04T21:39:45.086Z",
    "size": 18956
  },
  "/robots.txt": {
    "type": "text/plain; charset=utf-8",
    "etag": "\"8-hMqyDrA8fJ0R904zgEPs3L55Jls\"",
    "mtime": "2023-03-04T21:39:45.086Z",
    "size": 8
  },
  "/video.mp4": {
    "type": "video/mp4",
    "etag": "\"9b943-4UwfQXKUjPCesGPr6J5j7GzNYGU\"",
    "mtime": "2023-03-04T21:39:45.085Z",
    "size": 637251
  }
}
```

This allows Nitro to know the public assets without scanning the directory, giving high performance with caching headers.

### Custom Public Asset Directories

You can configure additional public asset directories using the `publicAssets` config option. Each entry supports the following properties:

- `dir` -- Path to the directory (resolved relative to `rootDir`).
- `baseURL` -- URL prefix for serving assets (default: `"/"`).
- `maxAge` -- Cache `max-age` in seconds. When set, a `Cache-Control: public, max-age=<value>, immutable` header is applied via route rules. Only takes effect when `fallthrough` is `false` (the default for non-root directories).
- `fallthrough` -- Whether requests should fall through to application handlers when the asset is not found. Top-level (`baseURL: "/"`) directories default to `true`; non-root directories default to `false`.
- `ignore` -- Pass `false` to disable ignore patterns, or an array of glob patterns to override the global `ignore` option.

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

export default defineConfig({
  publicAssets: [
    {
      baseURL: "build",
      dir: "public/build",
      maxAge: 3600,
    },
  ],
});
```

In this example, files in `public/build/` are served under `/build/` with a one-hour cache and no fallthrough to application handlers.

### Compressed Public Assets

Nitro can generate pre-compressed versions of your public assets during the build. When a client sends an `Accept-Encoding` header, the server will serve the compressed version if available. Supported encodings are gzip (`.gz`), brotli (`.br`), and zstd (`.zst`).

Set `compressPublicAssets: true` to enable all encodings:

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

export default defineConfig({
  compressPublicAssets: true,
});
```

Or pick specific encodings:

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

export default defineConfig({
  compressPublicAssets: {
    gzip: true,
    brotli: true,
    zstd: false,
  },
});
```

::note
Only compressible MIME types (text, JavaScript, JSON, XML, WASM, fonts, SVG, etc.) with a file size of at least 1 KB are compressed. Source map files (`.map`) are excluded. zstd compression requires Node.js 22+ (`zlib.zstdCompress`) at build time and is silently skipped on older versions.
::

Assets written to the public directory by `close` hooks or other late build steps are also compressed at the end of the build.

## Importing Files

Any file can be imported with a `type` import attribute. Its contents are inlined into the server bundle as a `Uint8Array` with [`type: "bytes"`](https://github.com/tc39/proposal-import-bytes) or as a UTF-8 `string` with [`type: "text"`](https://github.com/tc39/proposal-import-text):

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

import logo from "./logo.png" with { type: "bytes" }; // Uint8Array
import readme from "./README.md" with { type: "text" }; // string

export default defineHandler(() => {
  return new Response(logo, { headers: { "content-type": "image/png" } });
});
```

The file type is ignored: any file can be imported as either bytes or text.

Dynamic imports are supported as well:

```ts
const { default: logo } = await import("./logo.png", { with: { type: "bytes" } });
```

Alternatively, the `raw:` prefix imports a file as a `string` (for text files, based on the file type) or a `Uint8Array` (for binary files):

```ts
import readme from "raw:./README.md"; // string
import logo from "raw:./logo.png"; // Uint8Array
```

::note
Imported files are inlined into the server bundle (base64 encoded for binary files). Prefer [server assets](#server-assets) for larger files.
::

::note
TypeScript does not support the `bytes` and `text` attributes yet. Until then, imports of unknown file types need a `// @ts-ignore` comment.
::

## Server Assets

All assets in the `assets/` directory (relative to [`serverDir`](/config#serverdir) when it is set, otherwise the project root) will be added to the server bundle. After building your application, you can find them in the `.output/server/chunks/raw/` directory. Be careful with the size of your assets, as they will be bundled with the server bundle.

::tip
If your code never uses `useKV()`, server assets won't be included in the server bundle.
::

They can be addressed by the `assets:server` mount point using the [storage layer](/docs/storage).

For example, you could store a json file in `assets/data.json` and retrieve it in your handler:

```js
import { defineHandler } from "nitro";
import { useKV } from "nitro/kv";

export default defineHandler(async () => {
  const data = await useKV("assets:server").get("data.json");

  return data;
});
```

### Custom Server Assets

In order to add assets from a custom directory, you will need to define a path in your nitro config. This allows you to add assets from a directory outside of the `assets/` directory.

Each entry in `serverAssets` supports the following properties:

- `baseName` -- Name used as the storage mount point (accessed via `assets:<baseName>`).
- `dir` -- Path to the directory (resolved relative to `rootDir`).
- `pattern` -- Glob pattern for file inclusion (default: `"**/*"`).
- `ignore` -- Array of glob patterns to exclude files.

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

export default defineConfig({
  serverAssets: [
    {
      baseName: "templates",
      dir: "./templates",
    },
  ],
});
```

Then you can use the `assets:templates` base to retrieve your assets.

```ts [handlers/success.ts]
import { defineHandler } from "nitro";
import { useKV } from "nitro/kv";

export default defineHandler(async (event) => {
  const html = await useKV("assets:templates").get("success.html");

  return html;
});
```

::note
During development, server assets are read directly from the filesystem using the `fs` unstorage driver. In production, they are bundled into the server as lazy imports with pre-computed metadata (MIME type, ETag, modification time).
::

::read-more{to="/docs/storage"}
Server assets are accessed through the storage layer. See the storage docs for the full list of available methods.
::

---

### 1.Docs 50.Configuration (`docs/1.docs/50.configuration.md`)

---
icon: ri:settings-3-line
---

# Configuration

> Customize and extend Nitro defaults.

::read-more{to="/config"}
See [config reference](/config) for available options.
::

## Config file

You can customize Nitro with a configuration file. Depending on your setup, define options in `nitro.config.ts`, or under the `nitro` key in `vite.config.ts` when using the Vite plugin.

::code-group
```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
  }
})
```
::

### Supported configuration files

Nitro loads its configuration using [c12](https://github.com/unjs/c12) from the project root directory (the directory passed to the `nitro` CLI, defaulting to the current working directory). The following locations are checked in order and the first matching file is used:

- `nitro.config.{js,ts,mjs,cjs,mts,cts,json,jsonc,json5,yaml,yml,toml}`
- `.config/nitro.{js,ts,mjs,cjs,mts,cts,json,jsonc,json5,yaml,yml,toml}`
- `.config/nitro.config.{js,ts,mjs,cjs,mts,cts,json,jsonc,json5,yaml,yml,toml}`

Using `nitro.config.ts` is the recommended convention.

An extensionless `.nitrorc` file in the same directory is also loaded (using `key=value` syntax) and merged with lower priority than the main configuration file.

When using the Vite plugin, options can also be passed inline with the `nitro` key in `vite.config.ts` (as shown above).

### Environment-specific config

Using [c12](https://github.com/unjs/c12) conventions, you can provide environment-specific overrides using `$development` and `$production` keys:

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

export default defineConfig({
  logLevel: 3,
  $development: {
    // Options applied only in development mode
    debug: true,
  },
  $production: {
    // Options applied only in production builds
    minify: true,
  },
})
```

The environment name is `"development"` during `nitro dev` and `"production"` during `nitro build`.

### Extending configs

You can extend from other configs or presets using the `extends` key:

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

export default defineConfig({
  extends: "./base.config",
})
```

Resolved layers are available to [modules](/docs/modules) as `nitro.options.layers`, ordered from highest to lowest priority. The first layer is always the project root. Each layer has a `cwd` (layer directory), an optional `configFile`, its own `config` (before merging), and an optional `meta`. Presets are not included.

```ts [modules/layers.ts]
import type { NitroModule } from "nitro/types";

export default {
  setup(nitro) {
    for (const layer of nitro.options.layers) {
      nitro.logger.info(`Layer: ${layer.meta?.name || layer.cwd}`);
    }
  },
} satisfies NitroModule;
```

## Directory options

Nitro provides several options for controlling directory structure:

| Option | Default | Description |
| --- | --- | --- |
| `rootDir` | `.` (current directory) | The root directory of the project. |
| `serverDir` | `false` | Server source directory (set to `"server"` or `"./"` to enable). |
| `buildDir` | `node_modules/.nitro` | Directory for build artifacts. |
| `output.dir` | `.output` | Production output directory. |
| `output.serverDir` | `.output/server` | Server output directory. |
| `output.publicDir` | `.output/public` | Public assets output directory. |

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

export default defineConfig({
  serverDir: "server",
  buildDir: "node_modules/.nitro",
  output: {
    dir: ".output",
  },
})
```

::note
The `srcDir` option is deprecated. Use `serverDir` instead.
::

:read-more{to="/config#directories"}

## Environment variables

Certain Nitro behaviors can be configured using environment variables. These variables configure Nitro itself at build and startup time. To expose your own values to the application, use [runtime configuration](#runtime-configuration) instead.

| Variable | Description |
| --- | --- |
| `NITRO_PRESET` | Override the deployment preset. |
| `NITRO_COMPATIBILITY_DATE` | Set the compatibility date. |
| `NITRO_APP_BASE_URL` | Override the base URL (default: `/`). |
| `NITRO_BUILDER` | Select the bundler to use for building (`rollup`, `rolldown`, or `vite`). |
| `NITRO_DEV_RELOAD_TIMEOUT` | How long (in milliseconds) `vite dev` requests wait for the server entry to finish loading or reloading before they fall back to the previous entry, or return a 503 if none has loaded yet (default: `120000`). |
| `NITRO_ENV_PREFIX` | Set a custom secondary prefix for runtime config environment overrides (default: `_`). |
| `NITRO_ENV_EXPANSION` | Enable environment variable expansion in runtime config values. |

## Runtime configuration

Runtime config lets you expose configuration values to your application and override them at runtime using environment variables. This is useful for values that differ between environments (development, staging, production), such as API endpoints or feature flags.

First, define the runtime config in your configuration file.

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

export default defineConfig({
  runtimeConfig: {
    apiToken: "dev_token", // `dev_token` is the default value
  }
});
```

You can now access the runtime config using `useRuntimeConfig()`.

```ts [api/example.get.ts]
import { defineHandler } from "nitro";
import { useRuntimeConfig } from "nitro/runtime-config";

export default defineHandler((event) => {
  return useRuntimeConfig().apiToken; // Returns `dev_token`
});
```

### Nested objects

Runtime config supports nested objects. Keys at any depth are mapped to environment variables using the `NITRO_` prefix and `UPPER_SNAKE_CASE` conversion:

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

export default defineConfig({
  runtimeConfig: {
    database: {
      host: "localhost",
      port: 5432,
    },
  },
});
```

```bash [.env]
NITRO_DATABASE_HOST="db.example.com"
NITRO_DATABASE_PORT="5433"
```

::note
Only keys defined in `runtimeConfig` in your config file will be considered. You cannot introduce new keys using environment variables alone.
::

### Serialization

Runtime config values must be serializable (strings, numbers, booleans, plain objects, and arrays). Non-serializable values (class instances, functions, etc.) will trigger a warning at build time.

Values that are `undefined` or `null` in the config are replaced with empty strings (`""`) as a fallback.

### Local development

You can update the runtime config using environment variables. You can use a `.env` or `.env.local` file locally and use platform variables in production (see below).

Create an `.env` file in your project root:

```bash [.env]
NITRO_API_TOKEN="123"
```

Restart the development server, fetch the `/api/example` endpoint, and you should see `123` as the response instead of `dev_token`.

::note
The `.env` and `.env.local` files are loaded when Nitro resolves your config, in both `nitro dev` and `nitro build`, so `nitro.config.ts` can read them via `process.env`. Variables already present in the environment take precedence over `.env` values. They are **not** loaded by the built server at runtime: in production, use your platform's native environment variable mechanism.
::

You can still access environment variables directly using `import.meta.env` or `process.env`, but avoid reading them in ambient global contexts (at the top level of modules) to prevent unexpected behavior.

### Production

You can define variables in your production environment to update the runtime config.

::warning
All variables must be prefixed with `NITRO_` to be applied to the runtime config. They will override the runtime config variables defined within your `nitro.config.ts` file.
::

```bash
NITRO_API_TOKEN="123"
```

Keys are camelCase in runtime config and UPPER_SNAKE_CASE in environment variables.

```ts
{
  helloWorld: "foo"
}
```

```bash
NITRO_HELLO_WORLD="foo"
```

### Custom env prefix

You can configure a secondary environment variable prefix using the `nitro.envPrefix` runtime config key. This prefix is checked in addition to the default `NITRO_` prefix:

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

export default defineConfig({
  runtimeConfig: {
    nitro: {
      envPrefix: "APP_",
    },
    apiToken: "",
  },
});
```

With this configuration, both `NITRO_API_TOKEN` and `APP_API_TOKEN` will be checked as overrides.

::note
When no custom prefix is configured, the default secondary prefix is `_`. For example, `_API_TOKEN` also overrides `apiToken`.
::

### Env expansion

When enabled, environment variable references using `{{VAR_NAME}}` syntax in runtime config string values are expanded at runtime:

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

export default defineConfig({
  experimental: {
    envExpansion: true,
  },
  runtimeConfig: {
    url: "https://{{APP_DOMAIN}}/api",
  },
});
```

```bash
APP_DOMAIN="example.com"
```

At runtime, `useRuntimeConfig().url` will resolve to `"https://example.com/api"`.

---

### 1.Docs 50.Database (`docs/1.docs/50.database.md`)

---
icon: ri:database-2-line
title: Database
---

> Nitro provides a built-in and lightweight SQL database layer.

The default database connection is **preconfigured** with [SQLite](https://db0.unjs.io/connectors/sqlite) and works out of the box for development mode and any Node.js compatible production deployments. By default, data will be stored in `.data/db.sqlite`.

:read-more{to="https://db0.unjs.io" title="DB0 Documentation"}

::important
Database support is currently experimental.
Refer to the [db0 issues](https://github.com/unjs/db0/issues) for status and bug report.
::

The database layer is opt-in. Enable the experimental feature flag:

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

export default defineConfig({
  experimental: {
    database: true
  }
})
```

`db0` is resolved from your project and installed automatically (without prompting) once `experimental.database` is enabled or you import `nitro/database`.

## Usage



```ts [server.ts]
import { defineHandler } from "nitro";
import { useDatabase } from "nitro/database";

export default defineHandler(async () => {
  const db = useDatabase();

  // Create users table
  await db.sql`DROP TABLE IF EXISTS users`;
  await db.sql`CREATE TABLE IF NOT EXISTS users ("id" TEXT PRIMARY KEY, "firstName" TEXT, "lastName" TEXT, "email" TEXT)`;

  // Add a new user
  const userId = String(Math.round(Math.random() * 10_000));
  await db.sql`INSERT INTO users VALUES (${userId}, 'John', 'Doe', '')`;

  // Query for users
  const { rows } = await db.sql`SELECT * FROM users WHERE id = ${userId}`;

  return {
    rows,
  };
});
```



### `useDatabase`

Use `useDatabase` to get a database instance. It accepts an optional connection name (defaults to `"default"`).

```ts
import { useDatabase } from "nitro/database";

// Use the default connection
const db = useDatabase();

// Use a named connection
const usersDb = useDatabase("users");
```

Database instances are created lazily on first use and cached for subsequent calls with the same connection name. If a connection name is not configured, an error will be thrown.

### `db.sql`

Execute SQL queries using tagged template literals with automatic parameter binding:

```ts
const db = useDatabase();

// Insert with parameterized values (safe from SQL injection)
const id = "1001";
await db.sql`INSERT INTO users VALUES (${id}, 'John', 'Doe', 'john@example.com')`;

// Query with parameters
const { rows } = await db.sql`SELECT * FROM users WHERE id = ${id}`;

// The result includes rows, changes count, and last insert ID
const result = await db.sql`INSERT INTO posts (title) VALUES (${"Hello"})`;
// result.rows, result.changes, result.lastInsertRowid
```

### `db.exec`

Execute a raw SQL string directly:

```ts
const db = useDatabase();

await db.exec("CREATE TABLE IF NOT EXISTS users (id TEXT PRIMARY KEY, name TEXT)");
```

### `db.prepare`

Prepare an SQL statement for repeated execution:

```ts
const db = useDatabase();

const stmt = db.prepare("SELECT * FROM users WHERE id = ?");
const result = await stmt.bind("1001").all();
```

## Configuration

You can change the default connection or define additional named connections to any of the [supported databases](https://db0.unjs.io/connectors/sqlite) using the `database` config, and integrate the database instance with any of the [supported ORMs](https://db0.unjs.io/integrations).

Each connection is a `DatabaseConnectionConfig` with a `connector` name and an optional `options` object. Connector-specific settings (such as `url`, `host`, or `name`) belong under `options`, not at the top level of the connection config.

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

export default defineConfig({
  database: {
    default: {
      connector: "sqlite",
      options: { name: "db" },
    },
    users: {
      connector: "postgresql",
      options: {
        url: "postgresql://username:password@hostname:port/database_name",
      },
    },
    analytics: {
      connector: "mysql2",
      options: {
        host: "localhost",
        port: 3306,
        user: "root",
        password: "password",
        database: "analytics",
      },
    },
  },
});
```

See the [db0 connector docs](https://db0.unjs.io/connectors) for the `options` each connector accepts.

### Development Database

Use the `devDatabase` config to override the database configuration **only for development mode**. This is useful for using a local SQLite database during development while targeting a different database in production.

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

export default defineConfig({
  database: {
    default: {
      connector: "postgresql",
      options: {
        url: "postgresql://username:password@hostname:port/database_name"
      }
    }
  },
  devDatabase: {
    default: {
      connector: "sqlite",
      options: { name: "dev-db" }
    }
  }
});
```

::note
In development, a provided `devDatabase` **replaces** the `database` configuration entirely; connections are not merged per-name. Include every connection you need during development in `devDatabase`.
::

::tip
When `experimental.database` is enabled and no `database` or `devDatabase` config is provided, Nitro automatically configures a default SQLite connection. In development mode, data is stored relative to the project root directory. In Node.js production, it uses the default SQLite path.
::

## Connectors

Nitro supports all [db0 connectors](https://db0.unjs.io/connectors). The `connector` field in the database config accepts any of the following values:

| Connector | Description |
|---|---|
| `sqlite` | Node.js built-in SQLite (alias for `node-sqlite`) |
| `node-sqlite` | Node.js built-in SQLite |
| `better-sqlite3` | [better-sqlite3](https://github.com/WiseLibs/better-sqlite3) |
| `sqlite3` | [sqlite3](https://github.com/TryGhost/node-sqlite3) |
| `bun` / `bun-sqlite` | Bun built-in SQLite |
| `libsql` / `libsql-node` | [libSQL](https://github.com/tursodatabase/libsql) (Node.js) |
| `libsql-http` | libSQL over HTTP |
| `libsql-web` | libSQL for web environments |
| `libsql-core` | libSQL with a user provided client |
| `postgresql` | [PostgreSQL](https://github.com/brianc/node-postgres) |
| `mysql2` | [MySQL](https://github.com/sidorares/node-mysql2) |
| `pglite` | [PGlite](https://github.com/electric-sql/pglite) (embedded PostgreSQL) |
| `planetscale` | [PlanetScale](https://github.com/planetscale/database-js) serverless |
| `neon` | [Neon](https://github.com/neondatabase/serverless) serverless |
| `cloudflare-d1` | [Cloudflare D1](https://developers.cloudflare.com/d1/) |
| `cloudflare-hyperdrive-mysql` | Cloudflare Hyperdrive with MySQL |
| `cloudflare-hyperdrive-postgresql` | Cloudflare Hyperdrive with PostgreSQL |

### Connector dependencies

Some connectors rely on a third-party library (for example, `postgresql` requires [`pg`](https://www.npmjs.com/package/pg)).

Nitro detects the libraries required by the configured connections and prompts to install the missing ones (installed automatically in CI). Installed libraries are then explicitly passed to the connector via its `lib` option, so that bundlers can statically resolve them.

---

### 1.Docs 50.Lifecycle (`docs/1.docs/50.lifecycle.md`)

---
icon: i-lucide-layers
---

# Lifecycle

> Understand how Nitro runs and serves incoming requests to your application.

## Server startup

When the server starts, before it accepts any request, Nitro:

1. Evaluates the top-level code of the [server entry](/docs/server-entry#startup-logic).
2. Creates the Nitro app and runs [runtime plugins](/docs/plugins) in order. Requests that arrive while [async plugins](/docs/plugins#async-plugins) are still running are held until they finish.

On serverless and edge presets, this happens on every cold start.

## Request lifecycle

A request can be intercepted and terminated (with or without a response) from any of these layers, in this order:

::steps

### `request` hook

The `request` hook is the first code that runs for every incoming request. It is registered via a [runtime plugin](/docs/plugins):

```ts [plugins/request-hook.ts]
import { definePlugin } from "nitro";

export default definePlugin((nitroApp) => {
  nitroApp.hooks.hook("request", (event) => {
    console.log(`Incoming request on ${event.req.url}`);
  });
});
```

::note
Errors thrown inside the `request` hook are captured by the [`error` hook](#error-handling) and do not terminate the request pipeline.
::

### Route rules

Matching [route rules](/docs/routing#route-rules) from the Nitro config execute next. Route rules run as middleware, before any global middleware, and most of them alter the response without terminating it (for instance, adding a header or setting a cache policy). The matched rules are resolved before any middleware runs and are available as `event.context.routeRules` in every middleware.

The `cache` (and `swr`) rule is the exception: it caches the route handler, so it runs after global and routed middleware, right before the matched route. Middleware runs on every request to a cached route, whether the response comes from the cache or not.

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

export default defineConfig({
  routeRules: {
    '/**': { headers: { 'x-nitro': 'first' } }
  }
})
```

:read-more{to="/docs/routing#route-rules" title="Routing > Route rules"}

### Static assets

When static asset serving is enabled (the default for most presets), Nitro registers a static assets handler as the **first global middleware**. It checks if the request matches a file in the `public/` directory before other global middleware and route handlers run.

If a match is found, the static file is served immediately with appropriate `Content-Type`, `ETag`, `Last-Modified`, and `Cache-Control` headers. The request is terminated and no further middleware or routes are executed.

Static assets also support content negotiation for pre-compressed files (gzip, brotli, zstd) via the `Accept-Encoding` header.

### Global middleware

Any global middleware defined in the `middleware/` directory runs next:

```ts [middleware/info.ts]
import { defineHandler } from "nitro";

export default defineHandler((event) => {
  event.context.info = { name: "Nitro" };
});
```

::warning
Returning from a middleware will close the request and should be avoided when possible.
::

::read-more{to="/docs/routing#middleware"}
Learn more about Nitro middleware.
::

### Routed middleware

Middleware registered for a specific route pattern (via the [`handlers` config](/docs/routing#route-scoped-middleware)) runs after global middleware. Any middleware attached to the matched route itself runs last, right before the route handler.

### Routes

Nitro matches the incoming request against the routes defined in the `routes/` folder.

```ts [routes/api/hello.ts]
export default (event) => ({ world: true })
```

::read-more{to="/docs/routing#filesystem-routing"}
Learn more about Nitro filesystem routing.
::

### Server entry

If a server entry is defined, it catches all requests not matched by any route, acting as a `/**` route handler.

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

export default defineHandler((event) => {
  if (event.url.pathname === "/") {
    return "Home page";
  }
});
```

::read-more{to="/docs/server-entry"}
Learn more about the Nitro server entry.
::

### Renderer

If no route is matched, Nitro will look for a renderer handler (defined or auto-detected) to handle the request.

::read-more{to="/docs/renderer"}
Learn more about Nitro renderer.
::

### `response` hook

After the response is created (from any of the layers above), the `response` hook runs. This hook receives the final `Response` object and the event, and can be used to inspect or modify response headers:

```ts [plugins/response-hook.ts]
import { definePlugin } from "nitro";

export default definePlugin((nitroApp) => {
  nitroApp.hooks.hook("response", (res, event) => {
    console.log(`Response ${res.status} for ${event.req.url}`);
  });
});
```

::note
The `response` hook runs for every response, including static assets, middleware-terminated requests, and error responses.
::

::

## Error handling

When an error occurs at any point in the request lifecycle, Nitro:

1. Calls the `error` hook with the error and context (including the event and source tags).
2. Passes the error to the **error handler** which converts it into an HTTP response.

### Throwing errors

Throw an `HTTPError` from any handler or middleware to end the request with a specific status code, message, and optional data:

```ts [routes/signup.ts]
import { defineHandler, HTTPError } from "nitro";

export default defineHandler((event) => {
  // Message and details
  throw new HTTPError("Invalid user input", { status: 400 });

  // Status code shortcut
  throw HTTPError.status(400, "Bad Request");

  // Full object form
  throw new HTTPError({
    status: 400,
    message: "Invalid user input",
    data: { field: "email" },
  });
});
```

::note
Any other thrown value (such as a plain `new Error()`) is treated as an **unhandled** error: the response is always `500` and the message, data, and stack are hidden from the client for security.
::

### Default error responses

In production, the default error handler always responds with JSON:

```json
{
  "error": true,
  "status": 400,
  "message": "Invalid user input",
  "data": { "field": "email" }
}
```

In development, programmatic requests receive the same JSON format, but when the request's `Accept` header includes `text/html` (browsers), Nitro renders an HTML error page with a source-mapped stack trace and highlighted code frames.

### Custom error handlers

To control error response formatting, set `errorHandler` in the Nitro config to a file that exports a handler with `defineErrorHandler`:

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

export default defineConfig({
  errorHandler: "~/error",
});
```

```ts [error.ts]
import { defineErrorHandler } from "nitro";

export default defineErrorHandler((error, event) => {
  return new Response(`[custom error] ${error.message}`, {
    status: error.status || 500,
    headers: { "Content-Type": "text/plain" },
  });
});
```

Return a `Response` to send it to the client, or return nothing to fall through to the next handler. `errorHandler` also accepts an array of handlers that run in order: the first one that returns a response wins, and the built-in default handler is always appended as the last entry, so there is always a fallback response:

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

export default defineConfig({
  errorHandler: ["~/error/known-errors", "~/error/fallback"],
});
```

If a handler itself throws, the failure is logged and the chain continues with the next handler. Each handler also receives the built-in `defaultHandler` as `{ defaultHandler }` in its third argument, so you can reuse the default rendering within your own logic.

::read-more{to="/examples/custom-error-handler"}
See a full custom error handler example.
::

To customize error rendering for the development server only (without affecting the production bundle), pass a handler function as `devErrorHandler`:

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

export default defineConfig({
  devErrorHandler: errorHandler,
});
```

### Observing errors

To observe errors without changing the response, use the `error` runtime hook from a [plugin](/docs/plugins):

```ts [plugins/errors.ts]
import { definePlugin } from "nitro";

export default definePlugin((nitroApp) => {
  nitroApp.hooks.hook("error", (error, context) => {
    console.error("Captured error:", error);
    // context.event - the H3 event (if available)
    // context.tags  - error source tags like "request", "response", "plugin"
  });
});
```

You can also feed errors into the same pipeline programmatically with [`nitroApp.captureError`](/docs/plugins#programmatic-error-capture). Errors are also tracked per-request in `event.req.context.nitro.errors` for inspection in later hooks.

Additionally, unhandled promise rejections and uncaught exceptions at the process level are automatically captured into the `error` hook with the tags `"unhandledRejection"` and `"uncaughtException"`.

## Server shutdown

When the Nitro server is shutting down, the `close` hook is called. Use this to clean up resources such as database connections, timers, or external service handles:

```ts [plugins/cleanup.ts]
import { definePlugin } from "nitro";

export default definePlugin((nitroApp) => {
  nitroApp.hooks.hook("close", async () => {
    // Clean up resources
  });
});
```

::note
The hook is awaited after the HTTP server stops accepting connections. Long-running server
presets (Node.js, Bun and Deno) call it on `SIGINT` and `SIGTERM`. Serverless and edge presets
have no shutdown signal, so the hook only runs there if the platform closes the runtime.
::

## Hooks reference

All runtime hooks are registered through [runtime plugins](/docs/plugins) using `nitroApp.hooks.hook()`.

| Hook | Signature | When it runs |
| --- | --- | --- |
| `request` | `(event: HTTPEvent) => void \| Promise<void>` | Start of each request, before routing. |
| `response` | `(res: Response, event: HTTPEvent) => void \| Promise<void>` | After the response is created, before it is sent. |
| `error` | `(error: Error, context: { event?, tags? }) => void` | When any error is captured during the lifecycle. |
| `close` | `() => void` | When the Nitro server is shutting down. |

::note
The `NitroRuntimeHooks` interface is augmentable. Deployment presets (such as Cloudflare) can extend it with platform-specific hooks.
::

::read-more{to="/docs/plugins"}
Learn more about Nitro plugins and hook usage examples.
::

---

### 1.Docs 50.Openapi (`docs/1.docs/50.openapi.md`)

---
icon: ri:contract-line
---

# OpenAPI

> Nitro can automatically generate an [OpenAPI](https://www.openapis.org/) specification from your route handlers and serve interactive API documentation.

Nitro scans all route handlers, extracts metadata defined with `defineRouteMeta`, and generates an [OpenAPI 3.1.0](https://spec.openapis.org/oas/v3.1.0) specification. Built-in UIs powered by [Scalar](https://scalar.com/) and [Swagger UI](https://swagger.io/tools/swagger-ui/) let you explore and test your API directly in the browser.

::important
OpenAPI support is currently experimental.
::

## Enable OpenAPI

Enable OpenAPI in your Nitro configuration:

::code-group
```ts [nitro.config.ts]
import { defineConfig } from "nitro";

export default defineConfig({
  experimental: {
    openAPI: true,
  },
});
```
::

Once enabled, the following endpoints become available during development:

| Endpoint | Description |
|---|---|
| `/_openapi.json` | OpenAPI 3.1.0 JSON specification |
| `/_scalar` | Scalar API reference UI |
| `/_swagger` | Swagger UI |

## Route Metadata

Use the `defineRouteMeta` macro in route handler files to provide OpenAPI metadata for each route. The `openAPI` property accepts a standard OpenAPI [Operation Object](https://spec.openapis.org/oas/v3.1.0#operation-object).

```ts [routes/api/hello.ts]
import { defineRouteMeta, defineHandler } from "nitro";

defineRouteMeta({
  openAPI: {
    tags: ["greeting"],
    description: "Returns a greeting message",
    responses: {
      200: { description: "Successful greeting" },
    },
  },
});

export default defineHandler(() => {
  return { message: "Hello, world!" };
});
```

::note
`defineRouteMeta` is a build-time macro. The metadata is statically extracted during the build and does not add any runtime overhead to your handlers.
::

### Parameters

Route parameters (`:id`, `[id]`) are automatically converted to OpenAPI path parameters. You can define additional query or header parameters in the `parameters` array:

```ts [routes/api/users/[id].get.ts]
import { defineRouteMeta, defineHandler } from "nitro";

defineRouteMeta({
  openAPI: {
    tags: ["users"],
    description: "Get a user by their ID",
    parameters: [
      {
        in: "query",
        name: "include",
        description: "Comma-separated list of related resources to include",
        schema: { type: "string" },
      },
    ],
    responses: {
      200: { description: "User found" },
      404: { description: "User not found" },
    },
  },
});

export default defineHandler((event) => {
  const { id } = event.context.params;
  return { id, name: "Alice" };
});
```

In this example, the `id` path parameter is automatically inferred from the route pattern. Only the additional `include` query parameter needs to be declared.

### Response Schemas

Define response content types and schemas using the standard OpenAPI `responses` object:

```ts [routes/api/status.ts]
import { defineRouteMeta, defineHandler } from "nitro";

defineRouteMeta({
  openAPI: {
    description: "Returns the current server status",
    responses: {
      200: {
        description: "Server status",
        content: {
          "application/json": {
            schema: {
              type: "object",
              properties: {
                status: { type: "string", enum: ["healthy", "degraded"] },
                uptime: { type: "number" },
              },
            },
          },
        },
      },
    },
  },
});

export default defineHandler(() => {
  return { status: "healthy", uptime: process.uptime() };
});
```

### Global Components

Use the `$global` property to define reusable schemas that are hoisted to the top-level `components` section of the OpenAPI specification. This lets you reference shared schemas with `$ref` across multiple routes.

```ts [routes/api/users.get.ts]
import { defineRouteMeta, defineHandler } from "nitro";

defineRouteMeta({
  openAPI: {
    tags: ["users"],
    description: "List all users",
    responses: {
      200: {
        description: "List of users",
        content: {
          "application/json": {
            schema: {
              type: "array",
              items: { $ref: "#/components/schemas/User" },
            },
          },
        },
      },
    },
    $global: {
      components: {
        schemas: {
          User: {
            type: "object",
            properties: {
              id: { type: "string" },
              name: { type: "string" },
              email: { type: "string", format: "email" },
            },
          },
        },
      },
    },
  },
});

export default defineHandler(() => {
  return [{ id: "1", name: "Alice", email: "alice@example.com" }];
});
```

Once defined, the `User` schema can be referenced from any other route with `{ $ref: "#/components/schemas/User" }` without re-declaring it.

### Automatic Tagging

Routes are automatically tagged based on their path prefix:

| Route prefix | Tag |
|---|---|
| `/api/` | API Routes |
| `/_` | Internal |
| Other | App Routes |

You can override this by specifying `tags` in the `openAPI` metadata.

## Configuration

Configure OpenAPI behavior with the top-level `openAPI` option:

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

export default defineConfig({
  experimental: {
    openAPI: true,
  },
  openAPI: {
    meta: {
      title: "My API",
      description: "My awesome API",
      version: "2.0.0",
    },
  },
});
```

### `meta`

Set the API metadata that appears in the specification's `info` object:

| Property | Default | Description |
|---|---|---|
| `title` | `"Nitro Server Routes"` | API title |
| `description` | — | API description |
| `version` | `"1.0.0"` | API version |

### `route`

- Default: `"/_openapi.json"`

Override the path where the OpenAPI JSON specification is served:

```ts [nitro.config.ts]
export default defineConfig({
  openAPI: {
    route: "/_docs/openapi.json",
  },
});
```

### `ui`

Configure or disable the built-in API documentation UIs:

```ts [nitro.config.ts]
export default defineConfig({
  openAPI: {
    ui: {
      scalar: {
        route: "/_docs/scalar",
        theme: "purple",
      },
      swagger: {
        route: "/_docs/swagger",
      },
    },
  },
});
```

Set either UI to `false`{lang=ts} to disable it:

```ts [nitro.config.ts]
export default defineConfig({
  openAPI: {
    ui: {
      swagger: false,
    },
  },
});
```

:read-more{to="https://github.com/scalar/scalar" title="Scalar Configuration"}

## Production

By default, OpenAPI endpoints are only available during development. To enable them in production, set the `production` option:

```ts [nitro.config.ts]
export default defineConfig({
  openAPI: {
    production: "runtime",
  },
});
```

| Value | Behavior |
|---|---|
| `false`{lang=ts} | Disabled in production (default) |
| `"runtime"`{lang=ts} | Specification is generated at runtime on each request |
| `"prerender"`{lang=ts} | Specification is generated at build time and served as a static file |

Use `"prerender"`{lang=ts} when the specification does not change between deployments for the best performance. Use `"runtime"`{lang=ts} if you need dynamic server information or middleware access.

::warning
If you enable OpenAPI in production, make sure to protect the endpoints with appropriate authentication or access control.
::

---

### 1.Docs 50.Plugins (`docs/1.docs/50.plugins.md`)

---
icon: ri:plug-line
---

# Plugins

> Use runtime plugins to extend Nitro's runtime behavior.

Runtime plugins are **executed once** during server startup, making them the right place for one-time initialization and for registering [lifecycle hooks](/docs/lifecycle). Each plugin receives the `nitroApp` context.

Plugins are auto-registered from the `plugins/` directory and run in order. A plugin can also be [async](#async-plugins).

```ts [plugins/test.ts]
import { definePlugin } from "nitro";

export default definePlugin((nitroApp) => {
  console.log('Nitro plugin', nitroApp)
})
```

If you have plugins in another directory, you can use the `plugins` option:

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

export default defineConfig({
  plugins: ['my-plugins/hello.ts']
})
```

Plugins run in this order:

1. Plugins listed in the `plugins` option, in the order they are listed.
2. Plugins auto-registered from `plugins/` directories. Directories are processed in [`scanDirs`](/config#scandirs) order, and files within each directory are sorted by path.

## Async plugins

A plugin can return a promise, for example to connect to a database before the server handles traffic:

```ts [plugins/db.ts]
import { definePlugin } from "nitro";

export default definePlugin(async (nitroApp) => {
  const db = await connectToDatabase();
  nitroApp.hooks.hook("close", () => db.close());
});
```

The plugins after an async plugin run once it resolves, so they can rely on what it set up. Until all plugins have finished:

- Incoming requests are held and handled once initialization completes.
- Platform events dispatched as hooks (such as `cloudflare:scheduled` or `cloudflare:queue`) and scheduled tasks are deferred as well.
- `nitroApp.ready` is a promise that resolves when initialization completes. It is `undefined` once the app is ready.

This does not rely on top-level `await`, so it works on every deployment target. Once all plugins have resolved, requests are handled without any extra overhead.

If a plugin rejects, the plugins after it do not run. The error is passed to the `error` hook and reported as an unhandled rejection (like an error thrown by a sync plugin), and requests fail with a `500` response from then on.

::warning
Do not `await` a request to the app itself (such as `fetch("/api/...")`) inside an async plugin: requests are held until the plugin resolves, so it would never complete.
::

::note
Platforms such as Cloudflare Workers do not allow I/O (like `fetch()` or opening sockets) outside of a request. Plugins run at startup, so on these platforms set up connections lazily (on first use) instead.
::

## The `nitroApp` context

The plugin function receives a `nitroApp` object with the following properties:

| Property | Type | Description |
| --- | --- | --- |
| `hooks` | [`HookableCore`](https://github.com/unjs/hookable) | Hook system for registering lifecycle callbacks. |
| `h3` | `H3Core` | The underlying [H3](https://github.com/h3js/h3) application instance. |
| `fetch` | `(req: Request) => Response \| Promise<Response>` | The app's internal fetch handler. |
| `captureError` | `(error: Error, context) => void` | Programmatically capture errors into the error hook pipeline. |
| `ready` | `Promise<void> \| undefined` | Set while [async plugins](#async-plugins) are initializing. |

::note
H3 composes the middleware chain once, on the first request. Middleware a plugin adds to `nitroApp.h3["~middleware"]` during startup is included, but it runs before Nitro's route rules when placed at the front of the array. If a plugin changes the array after the first request was handled, it must also reset `nitroApp.h3["~dispatch"]` and `nitroApp.h3["~composed"]` to `undefined` so the chain is recomposed.
::

## Nitro runtime hooks

Use Nitro [hooks](https://github.com/unjs/hookable) to run custom functions at specific points in the [request lifecycle](/docs/lifecycle). Register them inside plugins with `nitroApp.hooks.hook()`:

```ts [plugins/close.ts]
import { definePlugin } from "nitro";

export default definePlugin((nitroApp) => {
  nitroApp.hooks.hook("close", async () => {
    // Will run when nitro is being closed
  });
})
```

### Available hooks

| Hook | Signature | Description |
| --- | --- | --- |
| `request` | `(event: HTTPEvent) => void \| Promise<void>` | Called at the start of each request. |
| `response` | `(res: Response, event: HTTPEvent) => void \| Promise<void>` | Called after the response is created. |
| `error` | `(error: Error, context: { event?: HTTPEvent, tags?: string[] }) => void` | Called when an error is captured. |
| `close` | `() => void` | Called when the Nitro server is shutting down. |

> [!NOTE]
> The `NitroRuntimeHooks` interface is augmentable. Deployment presets (such as Cloudflare) can extend it with platform-specific hooks like `cloudflare:scheduled` and `cloudflare:email`.

### Unregistering hooks

The `hook()` method returns an unregister function that can be called to remove the hook:

```ts [plugins/example.ts]
import { definePlugin } from "nitro";

export default definePlugin((nitroApp) => {
  const unregister = nitroApp.hooks.hook("request", (event) => {
    // ...
  });

  // Later, remove the hook
  unregister();
});
```

## Examples

### Capturing errors

You can use plugins to capture all application errors.

```ts [plugins/errors.ts]
import { definePlugin } from "nitro";

export default definePlugin((nitroApp) => {
  nitroApp.hooks.hook("error", async (error, { event }) => {
    console.error(`${event?.req.url} Application error:`, error)
  });
})
```

The `context` object includes an optional `tags` array that identifies the error source (e.g., `"request"`, `"response"`, `"cache"`, `"plugin"`, `"unhandledRejection"`, `"uncaughtException"`).

### Programmatic error capture

You can use `captureError` to manually feed errors into the error hook pipeline:

```ts [plugins/capture.ts]
import { definePlugin } from "nitro";

export default definePlugin((nitroApp) => {
  nitroApp.captureError(new Error("something went wrong"), {
    tags: ["startup"],
  });
});
```

### Graceful shutdown

The server shuts down gracefully, waiting for any pending background tasks started with `event.waitUntil`. Use the `close` hook to clean up your own resources:

```ts [plugins/shutdown.ts]
import { definePlugin } from "nitro";

export default definePlugin((nitroApp) => {
  nitroApp.hooks.hook("close", async () => {
    // Clean up resources, close connections, etc.
  });
});
```

### Request and response lifecycle

You can use plugins to register hooks that run on the [request lifecycle](/docs/lifecycle):

```ts [plugins/lifecycle.ts]
import { definePlugin } from "nitro";

export default definePlugin((nitroApp) => {
  nitroApp.hooks.hook("request", (event) => {
    console.log("on request", event.req.url);
  });

  nitroApp.hooks.hook("response", (res, event) => {
    // Modify or inspect the response
    console.log("on response", res.status);
  });
});
```

### Modifying response headers

```ts [plugins/headers.ts]
import { definePlugin } from "nitro";

export default definePlugin((nitroApp) => {
  nitroApp.hooks.hook("response", (res, event) => {
    const { pathname } = new URL(event.req.url);
    if (pathname.endsWith(".css") || pathname.endsWith(".js")) {
      res.headers.append("Vary", "Origin");
    }
  });
});
```

---

### 1.Docs 50.Prerendering (`docs/1.docs/50.prerendering.md`)

---
icon: ri:file-copy-2-line
---

# Prerendering

> Render routes at build time and serve them as static assets.

When prerendering is enabled, Nitro fetches the specified routes during the build and writes the responses to the `.output/public` directory. Prerendered routes are then served as static files. No server rendering happens at request time, which makes them fast, cacheable, and deployable to any static hosting or CDN.

Prerendering is a good fit for routes whose content does not change per request: landing pages, documentation, blog posts, or JSON endpoints with constant output. Dynamic routes can keep being served by the server, and you can freely mix prerendered and server-rendered routes in the same app.

## Prerendering routes

### `prerender.routes` config

List the routes to prerender using the `prerender.routes` option:

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

export default defineConfig({
  prerender: {
    routes: ["/", "/about", "/api/content.json"],
  },
});
```

HTML responses are written as `index.html` files within subfolders (`/about` produces `about/index.html`), while other responses keep their path (`/api/content.json` produces `api/content.json`). See [`autoSubfolderIndex`](#options) to change this behavior.

### `prerender` route rule

Alternatively, mark routes for prerendering with the `prerender` [route rule](/docs/routing#route-rules):

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

export default defineConfig({
  routeRules: {
    "/about": { prerender: true },
  },
});
```

::warning
Only **exact paths** are added to the prerender list from route rules. Wildcard patterns like `"/blog/**": { prerender: true }` are **not** auto-expanded, since Nitro cannot know which routes match the pattern. To prerender a dynamic set of routes, use [link crawling](#crawling-links) or the [`prerender:routes` hook](#hooks).
::

## Crawling links

Instead of (or in addition to) listing every route, enable `crawlLinks` to let Nitro discover routes by following links in prerendered HTML pages:

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

export default defineConfig({
  prerender: {
    crawlLinks: true,
    routes: ["/"],
  },
});
```

The crawler extracts `href` attributes from each prerendered HTML page and queues the discovered routes for prerendering. External links, hash links, and links to files with extensions (except `.json`) are skipped. Relative links are resolved against the page they were found on.

If `crawlLinks` is enabled and no routes are specified, crawling starts from `/`.

::tip
Handlers can queue additional routes for prerendering by setting the `x-nitro-prerender` response header to a comma-separated list of paths. This works even when `crawlLinks` is disabled and also applies to non-HTML responses, such as JSON.
::

## Ignoring routes

Use `prerender.ignore` to skip routes. Each entry can be a string prefix, a regular expression, or a function receiving the path:

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

export default defineConfig({
  prerender: {
    crawlLinks: true,
    ignore: [
      "/admin",                        // Ignore routes starting with /admin
      /\/preview$/,                     // Ignore routes ending with /preview
      (path) => path.includes("draft"), // Ignore routes containing "draft"
    ],
  },
});
```

You can also exclude routes with the `prerender: false` route rule. Unlike enabling prerendering, wildcard patterns **do** work for exclusion since they are matched against each candidate route:

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

export default defineConfig({
  routeRules: {
    "/dynamic/**": { prerender: false },
  },
});
```

### Public assets

Routes served by [public assets](/docs/assets) with a base URL prefix are always skipped. Additionally, you can set `prerender.ignoreUnprefixedPublicAssets: true` to skip prerendering routes that are already served by unprefixed public assets, avoiding duplicate files in the output:

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

export default defineConfig({
  prerender: {
    crawlLinks: true,
    ignoreUnprefixedPublicAssets: true,
  },
});
```

## Options

| Option | Default | Description |
|--------|---------|-------------|
| `failOnError` | `false` | Fail the build when a route cannot be prerendered. Useful in CI. |
| `concurrency` | `1` | Maximum number of routes prerendered in parallel. |
| `interval` | `0` | Delay in milliseconds between prerender requests. |
| `autoSubfolderIndex` | `true` | Write HTML routes as subfolder indexes. |

The `concurrency` and `interval` options control prerendering speed. Increase `concurrency` to speed up large sites, or add an `interval` to avoid hitting rate limits when routes call external APIs.

`autoSubfolderIndex` controls how HTML files are written to `.output/public`:

```bash
# autoSubfolderIndex: true (default)
/about -> .output/public/about/index.html
# autoSubfolderIndex: false
/about -> .output/public/about.html
```

Set it to `false` when your hosting provider does not let you configure trailing slash behavior.

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

export default defineConfig({
  prerender: {
    crawlLinks: true,
    failOnError: true,
    concurrency: 8,
    autoSubfolderIndex: false,
  },
});
```

## Hooks

For advanced use cases, you can hook into the prerendering lifecycle at build time. These hooks are available to [modules](/docs/modules) and the `hooks` config option:

| Hook | Description |
|------|-------------|
| `prerender:routes` | Called with the initial `Set` of routes, to add or remove entries before prerendering starts. |
| `prerender:config` | Called with the config of the internal Nitro instance used for prerendering. |
| `prerender:generate` | Called for each route before its file is written, to inspect or modify the route object (`route.contents`, `route.fileName`, ...) or set `route.skip = true`. |
| `prerender:route` | Called after each route is generated. |
| `prerender:done` | Called once prerendering is finished, with the lists of prerendered and failed routes. |

A common use case is registering routes dynamically, for example from a CMS or database:

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

export default defineConfig({
  modules: [
    (nitro) => {
      nitro.hooks.hook("prerender:routes", async (routes) => {
        const posts = await fetch("https://api.example.com/posts").then((res) => res.json());
        for (const post of posts) {
          routes.add(`/blog/${post.slug}`);
        }
      });
    },
  ],
});
```

## Static hosting

To ship a fully prerendered site without a server bundle, use a static preset such as `static`, `github_pages`, or `gitlab_pages`. These presets skip the server build, enable `crawlLinks`, and only output the `public` directory, ready to upload to any static hosting. See the [deploy documentation](/deploy) and providers like [GitHub Pages](/deploy/providers/github-pages) for details.

::note
The `static: true` config option only disables the server build; it does not enable prerendering by itself. When using it directly instead of a static preset, configure `prerender` options (typically `crawlLinks: true`) to generate the pages.
::

---

### 1.Docs 50.Tasks (`docs/1.docs/50.tasks.md`)

---
icon: codicon:run-all
---

# Tasks

> Nitro tasks allow one-off operations at runtime: named server-side actions such as database migrations, cache cleanup, or content syncing that you can run on demand, on a schedule, or from the CLI.

## Opt-in to the experimental feature

::important
Tasks support is currently experimental.
See [nitrojs/nitro#1974](https://github.com/nitrojs/nitro/issues/1974) for the relevant discussion.
::

In order to use the tasks API you need to enable the experimental feature flag. Tasks are scanned from your [`serverDir`](/docs/configuration), which has to be set as well:

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

export default defineConfig({
  serverDir: "./server",
  experimental: {
    tasks: true
  }
})
```

All examples on this page assume this config.

## Define tasks

Tasks can be defined in `tasks/[name].ts` files inside your [`serverDir`](/docs/configuration).

::note
Like routes and plugins, tasks are only scanned when [`serverDir`](/docs/configuration) (or `scanDirs`) is set; no directories are scanned by default.
::

Nested directories are supported. The task name will be joined with `:`.

```txt
server/
  tasks/
    cleanup.ts       <-- cleanup
    db/
      migrate.ts     <-- db:migrate
nitro.config.ts
```

**Example:**

```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 task...");
    return { result: "Success" };
  },
});
```

### Task interface

The `defineTask` helper accepts an object with the following properties:

- **`meta`** (optional): An object with optional `name` and `description` string fields used for display in the dev server and CLI.
- **`run`** (required): A function that receives a [`TaskEvent`](#taskevent) and returns (or resolves to) an object with an optional `result` property.

```ts
interface Task<RT = unknown> {
  meta?: { name?: string; description?: string };
  run(event: TaskEvent): { result?: RT } | Promise<{ result?: RT }>;
}
```

### `TaskEvent`

The `run` function receives a `TaskEvent` object with the following properties:

- **`name`**: The name of the task being executed.
- **`payload`**: An object (`Record<string, unknown>`) containing any data passed to the task.
- **`context`**: A `TaskContext` object (may include `waitUntil` depending on the runtime).

```ts
interface TaskEvent {
  name: string;
  payload: TaskPayload;
  context: TaskContext;
}
```

### Registering tasks via config

In addition to file-based scanning, tasks can be registered directly in the Nitro config. This is useful for adding a description to a scanned task, or for tasks provided by modules and pointing to custom handler paths.

```ts [nitro.config.ts]
import { fileURLToPath } from "node:url";
import { defineConfig } from "nitro";

export default defineConfig({
  serverDir: "./server",
  experimental: {
    tasks: true
  },
  tasks: {
    // Describe a task scanned from `server/tasks/db/migrate.ts`
    "db:migrate": {
      description: "Run database migrations"
    },
    // Register a task from a custom location
    "db:seed": {
      handler: fileURLToPath(new URL("scripts/seed.ts", import.meta.url)),
      description: "Seed the database"
    }
  }
})
```

::warning
Unlike route handlers, a task `handler` is imported as-is and is **not** resolved against the project root, so a path such as `"./scripts/seed.ts"` will fail to resolve at build time. Use an absolute path (for example via `fileURLToPath(new URL(..., import.meta.url))`) or a package name.
::

If a task is both scanned from the `tasks/` directory and defined in the config, the config-defined `handler` takes precedence.

## Scheduled tasks

You can define scheduled tasks using Nitro configuration to automatically run at the specified cron-formatted times.

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

export default defineConfig({
  scheduledTasks: {
    // Run `cms:update` task every minute
    '* * * * *': ['cms:update'],
    // Run a single task (string shorthand)
    '0 * * * *': 'db:cleanup'
  }
})
```

The `scheduledTasks` config maps cron expressions to either a single task name (string) or an array of task names. When multiple tasks are assigned to the same cron expression, they run in parallel.

::tip
You can use [crontab.guru](https://crontab.guru/) to easily generate and understand cron tab patterns.
::

When a scheduled task runs, it automatically receives a `payload` with `scheduledTime` set to the current timestamp (`Date.now()`).

::note
Prefer `scheduledTasks` when your deployment target supports it (see [platform support](#platform-support)). Nitro wires up the platform's native scheduler for you. If your platform is not supported, you can still trigger tasks from an external scheduler by calling [`runTask`](#programmatically-run-tasks) from an authenticated HTTP endpoint.
::

### Platform support

- **`dev`**, **`node_server`**, **`node_cluster`**, **`node_middleware`**, **`bun`** and **`deno_server`** presets are supported by the [croner](https://croner.56k.guru/) engine. Schedules stop when the Nitro `close` hook runs. With `node_middleware`, Nitro does not own the server, so call `useNitroHooks().callHook("close")` when your host server shuts down.
- **`cloudflare_module`** and **`cloudflare_pages`** presets have native integration with [Cron Triggers](https://developers.cloudflare.com/workers/configuration/cron-triggers/). Nitro automatically generates the cron triggers in the wrangler config at build time - no manual wrangler setup required.
- **`vercel`** preset has native integration with [Vercel Cron Jobs](https://vercel.com/docs/cron-jobs). Nitro automatically generates the cron job configuration at build time - no manual `vercel.json` setup required. You can secure cron endpoints by setting the `CRON_SECRET` environment variable.
- More presets (with native primitives support) are planned to be supported!

## `waitUntil`

When running background tasks, you might want to make sure the server or worker waits until the task is done.

An optional `context.waitUntil` function _might_ be available depending on the runtime.

```ts
import { defineTask } from "nitro/task";

export default defineTask({
  run({ context }) {
    const promise = fetch(...)
    context.waitUntil?.(promise);
    await promise;
    return { result: "Success" };
  },
});
```

## Programmatically run tasks

To manually run tasks, you can use the `runTask(name, { payload?, context? })` function from `nitro/task`.

**Example:**

```ts [server/api/migrate.ts]
import { defineHandler } from "nitro";
import { runTask } from "nitro/task";

export default defineHandler(async (event) => {
  // IMPORTANT: Authenticate user and validate payload!
  const payload = Object.fromEntries(event.url.searchParams);
  const { result } = await runTask("db:migrate", { payload });

  return { result };
});
```

### Error handling

`runTask` throws an HTTP error if:

- The task does not exist (status `404`).
- The task has no handler implementation (status `501`).

Any errors thrown inside the task's `run` function will propagate to the caller.

## Run tasks with dev server

Nitro's built-in dev server exposes tasks to be easily executed without programmatic usage.

### Using API routes

#### `/_nitro/tasks`

This endpoint returns a list of available task names and their meta.

```json
// [GET] /_nitro/tasks
{
  "tasks": {
    "db:migrate": {
      "description": "Run database migrations"
    },
     "cms:update": {
      "description": "Update CMS content"
    }
  },
  "scheduledTasks": [
    {
      "cron": "* * * * *",
      "tasks": [
        "cms:update"
      ]
    }
  ]
}
```

#### `/_nitro/tasks/:name`

This endpoint executes a task. You can provide a payload using both query parameters and body JSON payload. The payload sent in the JSON body payload must be under the `"payload"` property.

::code-group
```ts [server/tasks/echo/payload.ts]
import { defineTask } from "nitro/task";

export default defineTask({
  meta: {
    name: "echo:payload",
    description: "Returns the provided payload",
  },
  run({ payload, context }) {
    console.log("Running echo task...");
    return { result: payload };
  },
});
```
```json [GET]
// [GET] /_nitro/tasks/echo:payload?field=value&array=1&array=2
{
  "field": "value",
  "array": ["1", "2"]
}
```
```json [POST]
/**
 * [POST] /_nitro/tasks/echo:payload?field=value
 * body: {
 *   "payload": {
 *     "answer": 42,
 *     "nested": {
 *       "value": true
 *     }
 *   }
 * }
 */
{
  "field": "value",
  "answer": 42,
  "nested": {
    "value": true
  }
}
```
::

::note
Keys specified in the JSON body overwrite any values present in the query parameters.
::

### Using the CLI

::important
It is only possible to run these commands while the **dev server is running**. You should run them in a second terminal.
::

#### List tasks

```sh
nitro task list
```

#### Run a task

```sh
nitro task run db:migrate --payload "{}"
```

The `--payload` flag accepts a JSON string that will be parsed and passed to the task. If the value is not a valid JSON object, the task runs without a payload.

::read-more{to="/docs/cli#nitro-task"}
See the CLI reference for all `nitro task` subcommands and flags.
::

## Notes

### Concurrency

Task runs are deduplicated by **task name**: each task can have at most one running instance per server instance. Calling `runTask` for a task that is already running does not start a new run: all concurrent callers, even those passing different payloads, share the result of the first in-flight run.

---

### 1.Docs 50.Websocket (`docs/1.docs/50.websocket.md`)

---
icon: ri:broadcast-fill
---

# WebSocket

> Nitro provides cross-platform WebSocket support powered by [CrossWS](https://crossws.h3.dev/) and [H3](https://h3.dev/).

WebSocket enables real-time, bidirectional communication between client and server. Nitro's WebSocket integration works across all supported deployment targets including Node.js, Bun, Deno, Vercel and Cloudflare Workers.

::tip
If you only need one-way server-to-client updates (notifications, live feeds), consider [Server-Sent Events](#server-sent-events-sse) instead. They use plain HTTP and reconnect automatically.
::

:read-more{to="https://crossws.h3.dev/" title="CrossWS Documentation"}

## Enable WebSocket

Enable WebSocket support in your Nitro configuration:

::code-group
```ts [nitro.config.ts]
import { defineConfig } from "nitro";

export default defineConfig({
  features: {
    websocket: true,
  },
});
```
::

## Usage

Create a WebSocket handler using `defineWebSocketHandler` and export it from a route file. WebSocket handlers follow the same [file-based routing](/docs/routing) as regular request handlers.

```ts [routes/_ws.ts]
import { defineWebSocketHandler } from "nitro";

export default defineWebSocketHandler({
  open(peer) {
    console.log("Connected:", peer.id);
  },
  message(peer, message) {
    console.log("Message:", message.text());
    peer.send("Hello from server!");
  },
  close(peer, details) {
    console.log("Disconnected:", peer.id, details.code, details.reason);
  },
  error(peer, error) {
    console.error("Error:", error);
  },
});
```

::tip
You can use any route path for WebSocket handlers. For example, `routes/chat.ts` handles WebSocket connections on `/chat`.
::

### Connecting from the client

Use the browser's [WebSocket API](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) to connect:

```js
const ws = new WebSocket("ws://localhost:3000/_ws");

ws.addEventListener("open", () => {
  console.log("Connected!");
  ws.send("Hello from client!");
});

ws.addEventListener("message", (event) => {
  console.log("Received:", event.data);
});
```

## Hooks

WebSocket handlers accept the following lifecycle hooks:

### `upgrade`

Called before the WebSocket connection is established. Use it to authenticate requests, set the namespace, or attach context data to the peer.

```ts [routes/chat.ts]
import { defineWebSocketHandler } from "nitro";

export default defineWebSocketHandler({
  upgrade(request) {
    const url = new URL(request.url);
    const token = url.searchParams.get("token");
    if (!isValidToken(token)) {
      throw new Response("Unauthorized", { status: 401 });
    }
    return {
      context: { userId: getUserId(token) },
    };
  },
  open(peer) {
    console.log("User connected:", peer.context.userId);
  },
  // ...
});
```

The `upgrade` hook can return an object with:

| Property | Type | Description |
|---|---|---|
| `headers` | `HeadersInit` | Response headers to include in the upgrade response |
| `protocol` | `string` | Select the WebSocket subprotocol for the connection (must be one of the values the client offered in `Sec-WebSocket-Protocol`) |
| `namespace` | `string` | Override the pub/sub namespace for this connection |
| `context` | `object` | Data attached to `peer.context` |
| `handled` | `boolean` | When `true`, signals that the hook already performed the upgrade itself, so the adapter skips its own upgrade handling |

Throw a `Response` to reject the upgrade.

#### Subprotocols

Clients can offer one or more subprotocols when they connect. For example, GraphQL clients using the [`graphql-ws`](https://github.com/enisdenjo/graphql-ws) protocol open:

```js
const ws = new WebSocket("ws://localhost:3000/graphql", ["graphql-transport-ws"]);
```

Browsers reject the connection unless the server accepts one of the offered subprotocols. No subprotocol is accepted by default, so return the one your handler supports as `protocol` from the `upgrade` hook:

```ts [routes/graphql.ts]
import { defineWebSocketHandler } from "nitro";

export default defineWebSocketHandler({
  upgrade(request) {
    const offered = request.headers.get("sec-websocket-protocol")?.split(",") || [];
    if (offered.some((protocol) => protocol.trim() === "graphql-transport-ws")) {
      return { protocol: "graphql-transport-ws" };
    }
  },
  // ...
});
```

::note
Without an accepted subprotocol, Chrome fails with `Error during WebSocket handshake: Sent non-empty 'Sec-WebSocket-Protocol' header but no response was received`. Bun is an exception: it echoes the first offered subprotocol even when none is accepted.
::

:read-more{to="https://crossws.h3.dev/guide/hooks#subprotocol-negotiation" title="CrossWS Subprotocol Negotiation"}

### `open`

Called when a WebSocket connection is established and the peer is ready to send and receive messages.

```ts
open(peer) {
  peer.send("Welcome!");
}
```

### `message`

Called when a message is received from a peer.

```ts
message(peer, message) {
  const text = message.text();
  const data = message.json();
}
```

### `close`

Called when a WebSocket connection is closed. Receives a `details` object with optional `code` and `reason`.

```ts
close(peer, details) {
  console.log(`Closed: ${details.code} - ${details.reason}`);
}
```

### `error`

Called when an error occurs on the WebSocket connection.

```ts
error(peer, error) {
  console.error("WebSocket error:", error);
}
```

::note
Depending on the adapter, additional hooks such as `drain`, `ping`, and `pong` may be available. See the [CrossWS compatibility table](https://crossws.h3.dev/guide/peer#compatibility).
::

## Peer

The `peer` object represents a connected WebSocket client. It is available in all hooks except `upgrade`.

### Properties

| Property | Type | Description |
|---|---|---|
| `id` | `string` | Unique identifier for this peer |
| `namespace` | `string` | Pub/sub namespace this peer belongs to |
| `context` | `object` | Arbitrary context data set during `upgrade` |
| `request` | `Request` | The original upgrade request |
| `peers` | `Set<Peer>` | All connected peers in the same namespace |
| `topics` | `Set<string>` | Topics this peer is subscribed to |
| `remoteAddress` | `string?` | Client IP address (adapter-dependent) |
| `websocket` | `WebSocket` | The underlying WebSocket instance |

### Methods

#### `peer.send(data, options?)`

Send a message directly to this peer. Accepts strings, objects (serialized as JSON), or binary data.

```ts
peer.send("Hello!");
peer.send({ type: "greeting", text: "Hello!" });
```

#### `peer.subscribe(topic)`

Subscribe this peer to a pub/sub topic.

```ts
peer.subscribe("notifications");
```

#### `peer.unsubscribe(topic)`

Unsubscribe this peer from a topic.

```ts
peer.unsubscribe("notifications");
```

#### `peer.publish(topic, data, options?)`

Broadcast a message to all peers subscribed to a topic within the same namespace. The publishing peer does **not** receive the message.

```ts
peer.publish("chat", { user: "Alice", text: "Hello everyone!" });
```

#### `peer.close(code?, reason?)`

Gracefully close the WebSocket connection.

```ts
peer.close(1000, "Normal closure");
```

#### `peer.terminate()`

Immediately terminate the connection without sending a close frame.

## Message

The `message` object in the `message` hook provides methods to read the incoming data in different formats.

| Method | Return Type | Description |
|---|---|---|
| `text()` | `string` | Message as a UTF-8 string |
| `json()` | `T` | Message parsed as JSON |
| `uint8Array()` | `Uint8Array` | Message as a byte array |
| `arrayBuffer()` | `ArrayBuffer` | Message as an ArrayBuffer |
| `blob()` | `Blob` | Message as a Blob |

```ts
message(peer, message) {
  // Parse as text
  const text = message.text();

  // Parse as typed JSON
  const data = message.json<{ type: string; payload: unknown }>();
}
```

## Pub/Sub

Pub/sub (publish/subscribe) enables broadcasting messages to groups of connected peers through topics. Peers subscribe to topics and receive messages published to those topics.

```ts [routes/chat.ts]
import { defineWebSocketHandler } from "nitro";

export default defineWebSocketHandler({
  open(peer) {
    peer.subscribe("chat");
    peer.publish("chat", { system: `${peer} joined the chat` });
    peer.send({ system: "Welcome to the chat!" });
  },
  message(peer, message) {
    // Broadcast to all other subscribers
    peer.publish("chat", {
      user: peer.toString(),
      text: message.text(),
    });
    // Echo back to sender
    peer.send({ user: "You", text: message.text() });
  },
  close(peer) {
    peer.publish("chat", { system: `${peer} left the chat` });
  },
});
```

::note
`peer.publish()` sends the message to all subscribers of the topic **except** the publishing peer. Use `peer.send()` to also send to the publisher.
::

::tip
On Cloudflare Workers, pub/sub requires a [Durable Object](/deploy/providers/cloudflare#websockets-with-durable-objects).
::

### Namespaces

Namespaces provide isolated pub/sub groups for WebSocket connections. Each peer belongs to one namespace, and `peer.publish()` only broadcasts to peers within the same namespace.

By default, the namespace is derived from the request URL pathname. This works naturally with [dynamic routes](/docs/routing#dynamic-routes): each path gets its own isolated namespace:

```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) {
    // Only reaches peers in the same room
    peer.publish("messages", `${peer}: ${message.text()}`);
  },
  close(peer) {
    peer.publish("messages", `${peer} left`);
  },
});
```

In this example, clients connecting to `/rooms/game` are isolated from clients connecting to `/rooms/lobby`; each path is its own namespace.

To override the default namespace, return a custom `namespace` from the `upgrade` hook:

```ts [routes/chat.ts]
import { defineWebSocketHandler } from "nitro";

export default defineWebSocketHandler({
  upgrade(request) {
    // Group connections by a query parameter instead of the pathname
    const url = new URL(request.url);
    const channel = url.searchParams.get("channel") || "general";
    return {
      namespace: `chat:${channel}`,
    };
  },
  open(peer) {
    peer.subscribe("messages");
    peer.publish("messages", `${peer} joined`);
  },
  message(peer, message) {
    peer.publish("messages", `${peer}: ${message.text()}`);
  },
  close(peer) {
    peer.publish("messages", `${peer} left`);
  },
});
```

## Server-Sent Events (SSE)

[Server-Sent Events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events) provide a simpler alternative when you only need server-to-client streaming. Unlike WebSockets, SSE uses standard HTTP and supports automatic reconnection.

```ts [routes/sse.ts]
import { defineHandler } from "nitro";
import { createEventStream } from "nitro/h3";

export default defineHandler((event) => {
  const stream = createEventStream(event);

  const interval = setInterval(async () => {
    await stream.push(`Message @ ${new Date().toLocaleTimeString()}`);
  }, 1000);

  stream.onClosed(() => {
    clearInterval(interval);
  });

  return stream.send();
});
```

Connect from the client using the [EventSource API](https://developer.mozilla.org/en-US/docs/Web/API/EventSource):

```js
const source = new EventSource("/sse");

source.onmessage = (event) => {
  console.log(event.data);
};
```

### Structured messages

SSE messages support optional `id`, `event`, and `retry` fields:

```ts [routes/events.ts]
import { defineHandler } from "nitro";
import { createEventStream } from "nitro/h3";

export default defineHandler((event) => {
  const stream = createEventStream(event);
  let id = 0;

  const interval = setInterval(async () => {
    await stream.push({
      id: String(id++),
      event: "update",
      data: JSON.stringify({ value: Math.random() }),
      retry: 3000,
    });
  }, 1000);

  stream.onClosed(() => {
    clearInterval(interval);
  });

  return stream.send();
});
```

:read-more{to="https://h3.dev/" title="H3 Documentation"}

---

### 1.Docs 55.Modules (`docs/1.docs/55.modules.md`)

---
icon: i-lucide-puzzle
---

# Modules

> Use modules to extend Nitro's build-time behavior.

Nitro modules are functions that run **once when the Nitro instance is initialized** (in development, build, and prerendering). They receive the `nitro` build context, which they can use to modify options, register build-time hooks, add virtual files, or register route handlers.

::note
Modules run at **build time**. To extend the server's **runtime** behavior (request [lifecycle](/docs/lifecycle), error handling, shutdown), use [plugins](/docs/plugins) instead.
::

| | Modules | [Plugins](/docs/plugins) |
| --- | --- | --- |
| Run | Once, during initialization and build | Once, on server startup |
| Receive | `nitro` (build context) | `nitroApp` (runtime app) |
| Typical use | Modify config, add handlers and virtual files, hook into build | Hook into request/response lifecycle |
| Bundled into output | No | Yes |

## Using modules

Register modules with the `modules` config array:

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

export default defineConfig({
  modules: [
    // Path to a local module (relative to the project root)
    "./modules/my-module.ts",
    // Name of an npm package
    "my-nitro-module",
    // Inline module object
    {
      name: "inline-module",
      setup(nitro) {
        nitro.logger.info("Inline module setup");
      },
    },
    // Bare setup function
    (nitro) => {
      nitro.hooks.hook("compiled", () => {
        // ...
      });
    },
    // Plugin object with a `nitro` key
    {
      name: "plugin-shaped-module",
      nitro: {
        setup(nitro) {
          nitro.logger.info("Plugin module setup");
        },
      },
    },
  ],
});
```

Each entry can be one of the following forms:

| Form | Example | Description |
| --- | --- | --- |
| Path or package string | `"./modules/my-module.ts"`, `"my-nitro-module"` | Resolved from the project root and imported. The **default export** is used as the module. |
| Module object | `{ name?, setup }` | An object with a `setup(nitro)` function and an optional `name`. |
| Bare function | `(nitro) => { ... }` | A shorthand for `{ setup }`. |
| Plugin object | `{ name?, nitro: { setup } }` | An object with a `nitro` key holding the module. This matches the Rollup/Vite plugin shape, so one export can be both a bundler plugin and a Nitro module. |

Files in the `modules/` directory are automatically registered as modules (like `plugins/` for [runtime plugins](/docs/plugins)).

Only two patterns are auto-registered (with any supported extension: `.ts`, `.mts`, `.cts`, `.js`, `.mjs`, `.cjs`, `.tsx`, `.jsx`):

- `modules/*.ts` (a single-file module)
- `modules/*/index.ts` (a directory module)

Any other file inside `modules/` that does not match these patterns (for example a module's own `runtime/` files) is ignored by the scanner.

Modules referenced by path or package name are resolved and installed **only once**; duplicate entries pointing to the same file are skipped.

::tip
In a [Vite](/docs/vite) project, any plugin in `vite.config.ts` that carries a `nitro` key is registered as a Nitro module automatically. There is no need to also list it in `modules`.
::

## Authoring a module

A module is an object with a `setup` function receiving the [`Nitro`](https://github.com/nitrojs/nitro/blob/main/src/types/nitro.ts) instance. Use the `NitroModule` type from `nitro/types` for type safety:

```ts [modules/hello.ts]
import type { NitroModule } from "nitro/types";

export default {
  name: "hello",
  setup(nitro) {
    // Add a virtual module usable anywhere in the server code as `#hello`
    nitro.options.virtual["#hello"] = `export const hello = "world";`;

    // Register a route handler pointing to the virtual module
    nitro.options.handlers.push({
      route: "/_hello",
      handler: "#hello-handler",
    });
    nitro.options.virtual["#hello-handler"] = /* ts */ `
      import { defineHandler } from "nitro";
      import { hello } from "#hello";
      export default defineHandler(() => ({ hello }));
    `;
  },
} satisfies NitroModule;
```

The `setup` function can be async. Useful properties on the `nitro` instance:

| Property | Description |
| --- | --- |
| `nitro.options` | Resolved config (mutable): `handlers`, `virtual`, `plugins`, `runtimeConfig`, and every other option. |
| `nitro.hooks` | Build-time [hooks](#build-time-hooks) (`hookable` instance). |
| `nitro.logger` | Tagged [consola](https://github.com/unjs/consola) logger for build-time output. |
| `nitro.meta` | Nitro version info (`version`, `majorVersion`). |

## Build-time hooks

Modules can hook into the build lifecycle with `nitro.hooks.hook()`:

```ts [modules/build-info.ts]
import type { NitroModule } from "nitro/types";

export default {
  name: "build-info",
  setup(nitro) {
    nitro.hooks.hook("compiled", () => {
      nitro.logger.info(`Server built in ${nitro.options.output.dir}`);
    });
  },
} satisfies NitroModule;
```

The most useful hooks:

| Hook | Signature | When it runs |
| --- | --- | --- |
| `build:before` | `(nitro) => void` | Before the build starts, before the bundler config is created. |
| `build:config` | `(nitro, { builder, rollup?, rolldown? }) => void` | After the bundler config is resolved, right before bundling. Last chance to modify it in place. `builder` is `rollup`, `rolldown` or `vite`, and exactly one of `rollup` / `rolldown` is set (Vite passes the one for its bundler). Replaces the deprecated `rollup:before` hook. |
| `vite:compile:before` | `(nitro) => void` | Vite builder only. After public assets are copied and prerendering finished, before the build output is finalized (the server bundle is compiled, or skipped for static builds). Use it to emit extra files into the public output directory. |
| `compiled` | `(nitro) => void` | After the server bundle is written to the output directory. |
| `dev:start` | `() => void` | In development, when the dev worker starts. |
| `dev:reload` | `(payload?) => void` | In development, after each rebuild when the dev worker reloads. |
| `dev:error` | `(cause?) => void` | In development, when a build error occurs. |
| `build:reload` | `() => void` | When the Nitro config is hot-reloaded, to rebuild the server. Replaces the deprecated `rollup:reload` hook. |
| `prerender:routes` | `(routes: Set<string>) => void` | Before prerendering. Add or remove routes to prerender. |
| `prerender:config` | `(config) => void` | When the prerenderer's Nitro config is created. |
| `prerender:generate` | `(route, nitro) => void` | For each route, before it is generated. |
| `prerender:route` | `(route) => void` | For each route, after it is generated. |
| `prerender:done` | `({ prerenderedRoutes, failedRoutes }) => void` | After prerendering finishes. |
| `close` | `() => void` | When the Nitro instance is closed. |

All hooks can be async. See the [hooks source](https://github.com/nitrojs/nitro/blob/main/src/types/hooks.ts) for the full list and exact signatures, and the [prerendering guide](/docs/prerendering#hooks) for the `prerender:*` hooks in context.

::tip
Runtime hooks (`request`, `response`, `error`) are **not** available here; they belong to [plugins](/docs/plugins). A module can still add runtime behavior by pushing a plugin file to `nitro.options.plugins`.
::

## Best practices

- **Give every module a `name`.** It makes the module recognizable in config and, on a plugin object, is what Rollup and Vite require to identify the plugin.
- **Prefer the plugin object form.** `{ name, nitro: { setup } }` is understood by Nitro *and* by Rollup and Vite, so a single export can serve both. Users of a Vite app drop it into `plugins`, users of a standalone Nitro app into `modules`, and the module runs either way.
- **Keep modules idempotent.** In development, options can be re-processed on config changes, so check before pushing duplicate handlers, plugins, or virtual entries.
- **Prefer hooks over patching.** Use build hooks to react to lifecycle events instead of monkey-patching Nitro internals or overwriting functions.
- **Respect user options.** Read existing values from `nitro.options` and extend them instead of replacing them. Let users opt out of your module's behavior.
- **Use `nitro.logger`** for build-time output instead of `console`.
- **Mind the runtime bundle.** Any handler or plugin your module adds is bundled for every deployment target, so keep runtime code minimal and runtime-agnostic.

## Distributing modules

To share a module as an npm package, export the module object as the **default export** and users can reference it by package name:

```ts [my-nitro-module/src/index.ts]
import type { NitroModule } from "nitro/types";

export default {
  name: "my-nitro-module",
  setup(nitro) {
    // ...
  },
} satisfies NitroModule;
```

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

export default defineConfig({
  modules: ["my-nitro-module"],
});
```

Add `nitro` as a peer dependency, and import types only from `nitro/types` so your package does not bundle Nitro itself.

### Modules that are also bundler plugins

If your package already ships a Rollup or Vite plugin, put the Nitro module under a `nitro` key on the plugin object instead of publishing two entry points:

```ts [my-plugin/src/index.ts]
import type { Plugin } from "vite";
import type { NitroModule } from "nitro/types";

export default function myPlugin(): Plugin & { nitro: NitroModule } {
  return {
    name: "my-plugin",
    transform(code, id) {
      // ...
    },
    nitro: {
      setup(nitro) {
        nitro.options.virtual["#my-plugin"] = `export const value = "hello";`;
      },
    },
  };
}
```

The same export then works from both config files:

::code-group

```ts [vite.config.ts]
import { defineConfig } from "vite";
import { nitro } from "nitro/vite";
import myPlugin from "my-plugin";

export default defineConfig({
  plugins: [nitro(), myPlugin()],
});
```

```ts [nitro.config.ts]
import { defineConfig } from "nitro";
import myPlugin from "my-plugin";

export default defineConfig({
  modules: [myPlugin()],
});
```

::

Bundlers ignore the extra `nitro` key, and Nitro ignores the plugin hooks, so neither side needs to know about the other.

---

### 1.Docs 6.Server Entry (`docs/1.docs/6.server-entry.md`)

---
icon: ri:server-line
navigation:
  title: Server Entry
---

# Nitro Server Entry

> Use a server entry to handle every request that no route matched.

The server entry is a special handler that Nitro registers as a catch-all (`/**`) route. Specific routes always win, so the server entry only runs for requests none of them matched, right before the [renderer](/docs/renderer). It is commonly used to mount another framework inside Nitro, or to implement custom routing for everything the filesystem routes don't claim.

::warning
The server entry is a fallback, not a global middleware: it does not run for requests a route handled. For cross-cutting concerns that must apply to **every** request (authentication, logging, request preprocessing), use [middleware](/docs/routing#middleware) instead.
::

## Auto-detected `server.ts`

By default, Nitro automatically looks for a `server.ts` (or `.js`, `.mjs`, `.mts`, `.tsx`, `.jsx`) file in your `serverDir` (if set) or your project root directory.

If found, Nitro will use it as the server entry and run it for all incoming requests.

::code-group
```ts [server.ts]
export default {
  async fetch(req: Request) {
    const url = new URL(req.url);

    // Handle specific routes
    if (url.pathname === "/health") {
      return new Response("OK", {
        status: 200,
        headers: { "content-type": "text/plain" }
      });
    }

    // Add custom headers to all requests
    // Return nothing to continue to the next handler
  }
}
```
```ts [routes/api/hello.ts]
import { defineHandler } from "nitro";

export default defineHandler((event) => {
  return { hello: "API" };
});
```
::

::tip
When `server.ts` is detected, Nitro will log in the terminal: ``Detected `server.ts` as server entry.``
::

With this setup:
- `/health` → Handled by server entry (returns a response)
- `/api/hello` → Handled by the API route handler directly
- `/about`, etc. → Server entry runs first, then continues to the renderer if no response is returned

## Framework compatibility

The server entry is a great way to integrate with other frameworks. Any framework that exposes a standard Web `fetch(request: Request): Response` interface can be used as a server entry.

### Web-compatible frameworks

Frameworks that implement the Web `fetch` API work directly with `server.ts`:

::tabs
  ::tabs-item{label="H3" icon="i-undocs-h3"}
  ```ts [server.ts]
  import { H3 } from "h3";

  const app = new H3()

  app.get("/", () => "⚡️ Hello from H3!");

  export default app;
  ```
  ::
  ::tabs-item{label="Hono" icon="i-undocs-hono"}
  ```ts [server.ts]
  import { Hono } from "hono";

  const app = new Hono();

  app.get("/", (c) => c.text("🔥 Hello from Hono!"));

  export default app;
  ```
  ::
  ::tabs-item{label="Elysia" icon="i-undocs-elysia"}
  ```ts [server.ts]
  import { Elysia } from "elysia";

  const app = new Elysia();

  app.get("/", () => "🦊 Hello from Elysia!");

  export default app.compile();
  ```
  ::
::

### Node.js frameworks

For Node.js frameworks that use `(req, res)` style handlers (like [Express](https://expressjs.com/) or [Fastify](https://fastify.dev/)), name your server entry file `server.node.ts` instead of `server.ts`. Nitro will automatically detect the `.node.` suffix and convert the Node.js handler to a web-compatible fetch handler using [`srvx`](https://srvx.h3.dev/).

::tabs
  ::tabs-item{label="Express"}
  ```ts [server.node.ts]
  import Express from "express";

  const app = Express();

  app.use("/", (_req, res) => {
    res.send("Hello from Express with Nitro!");
  });

  export default app;
  ```
  ::
  ::tabs-item{label="Fastify"}
  ```ts [server.node.ts]
  import Fastify from "fastify";

  const app = Fastify();

  app.get("/", () => "Hello, Fastify with Nitro!");

  await app.ready();

  export default app.routing;
  ```
  ::
::


## Server options

When the server entry's default export is a plain object, the following server options are passed to the [srvx](https://srvx.h3.dev/) server started by the [Node.js](/deploy/runtimes/node), [Bun](/deploy/runtimes/bun) and [Deno](/deploy/runtimes/deno) presets: `port`, `hostname`, `reusePort`, `protocol`, `tls`, `silent`, `gracefulShutdown`, `maxRequestBodySize`, `trustProxy` and runtime specific settings (`node`, `bun`, `deno`).

Use the `defineServerEntry` helper for typed options:

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

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

::read-more{to="https://srvx.h3.dev/guide/options" title="srvx server options"}
::

::note
- `NITRO_PORT`/`PORT`, `NITRO_HOST`/`HOST` and `NITRO_SSL_CERT`/`NITRO_SSL_KEY` environment variables take precedence over the `port`, `hostname` and `tls` options, so the server stays configurable at runtime.
- To listen on a UNIX socket, set the `NITRO_UNIX_SOCKET` environment variable, or `node: { path }`, `bun: { unix }` or `deno: { path }` in the server entry.
- During development (`nitro dev`), options are applied to the dev worker server, except listener options (`port`, `hostname`, `protocol`, `tls`, `silent`, `gracefulShutdown`) which are controlled by the dev server and CLI (`--port`, `--host`). The Vite dev server applies none of them.
- Other srvx options (`middleware`, `plugins`, `error` and `manual`) are ignored. They change how requests are handled, but would only apply to these presets and not to serverless deployments or `nitroApp.fetch`. Use [Nitro middleware](/docs/routing#middleware) and [plugins](/docs/plugins) instead.
- Options are only read from plain object exports (not from framework instances like `export default app`) and never from Node.js format entries (`server.node.ts`).
::

## Configuration

### Custom server entry file

You can specify a custom server entry file using the `serverEntry` option in your Nitro configuration:

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

export default defineConfig({
  serverEntry: "./nitro.server.ts"
})
```

You can also provide an object with `handler` and `format` options:

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

export default defineConfig({
  serverEntry: {
    handler: "./server.ts",
    format: "node" // "web" (default) or "node"
  }
})
```

### Handler format

The `format` option controls how Nitro treats the default export of your server entry:

- **`"web"`** (default): Expects a Web-compatible handler with a `fetch(request: Request): Response` method.
- **`"node"`**: Expects a Node.js-style `(req, res)` handler. Nitro automatically converts it to a web-compatible handler.

When auto-detecting, the format is determined by the filename: `server.node.ts` uses `"node"` format, while `server.ts` uses `"web"` format.

### Disabling server entry

Set `serverEntry` to `false` to disable auto-detection and prevent Nitro from using any server entry:

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

export default defineConfig({
  serverEntry: false
})
```

## Using an event handler

Instead of a Web `fetch` handler, you can export an event handler made with `defineHandler` for better type inference and access to the H3 event object:

```ts [server.ts]
import { defineHandler, HTTPError } from "nitro";

export default defineHandler((event) => {
  // Runs only for requests no route matched
  if (event.url.pathname.startsWith("/api/")) {
    throw new HTTPError("Unknown API endpoint", { status: 404 });
  }

  // Add context for the renderer
  event.context.requestId = crypto.randomUUID();

  // Return nothing to hand the request over to the renderer
});
```

::important
Returning `undefined` (or nothing) hands the request to the [renderer](/docs/renderer). Without a renderer, Nitro answers with an empty `200` response. Returning a value ends the request there.
::

## Startup logic

The server entry is imported eagerly, so its top-level code runs **once when the server starts**, not on the first request.

Keep in mind:

- Top-level code runs **before** [runtime plugins](/docs/plugins), so hooks registered by plugins are not available yet. Do not call `useNitroApp()` at the top level; call it inside the handler instead.
- A top-level `await` delays startup: the server does not accept requests until the server entry has finished evaluating.
- On serverless and edge presets, "startup" means every cold start of a new instance. Some runtimes, like Cloudflare Workers, don't allow I/O (such as `fetch`) in the global scope.
- In development, top-level code runs again whenever the server entry is reloaded.

## Request lifecycle

The server entry is registered as a catch-all (`/**`) route handler. When a specific route (like `/api/hello`) matches a request, that route handler takes priority. For requests that don't match any specific route, the server entry runs before the renderer:

```md
1. Server hook: `request`
2. Route rules (headers, redirects, etc.)
3. Global middleware (static assets first, then middleware/)
4. Route-scoped middleware (handlers config)
5. Route matching:
   a. Specific routes (routes/) ← if matched, handles the request
   b. Server entry ← runs for unmatched routes
   c. Renderer (renderer.ts or index.html)
```

When both a server entry and a [renderer](/docs/renderer) exist, they are chained: the server entry runs first, and if it doesn't return a response, the renderer handles the request.

:read-more{to="/docs/lifecycle" title="Lifecycle"}

## Development mode

During development, Nitro watches for changes to your server entry file. When the file is created, modified, or deleted, the dev server automatically reloads to pick up the changes.

## Best practices

- Use the server entry as the fallback for requests no route matched, or to mount another framework
- Use [middleware](/docs/routing#middleware) for concerns that must apply to every request, matched routes included
- Return `undefined` to continue to the renderer; return a value to end the request
- Keep the server entry lightweight; it runs for every unmatched request
- Use [runtime plugins](/docs/plugins) for one-time initialization logic
- Don't use the server entry for route-specific logic; [route handlers](/docs/routing) are more performant

---

### 1.Docs 60.Cli (`docs/1.docs/60.cli.md`)

---
icon: i-lucide-terminal
---

# CLI

> Nitro ships with a `nitro` command to develop, build, preview, and deploy your project.

When `nitro` is installed as a project dependency, run it through your package manager (`npx nitro`, `pnpm nitro`, `yarn nitro`, `bun nitro`, ...) or add it to your `package.json` scripts:

```json [package.json]
{
  "scripts": {
    "dev": "nitro dev",
    "build": "nitro build",
    "preview": "nitro preview"
  }
}
```

Then use `npm run dev`, `pnpm dev`, etc. as shown in the [quick start](/docs/quick-start) guide.

::tip
Every command supports `--help` for an overview of its arguments (for example `nitro build --help`), and `nitro --version` prints the installed Nitro version.
::

All commands accept a project root directory, either as a positional argument (`nitro build ./my-app`) or with the `--dir` flag (preferred). It defaults to the current directory.

::note
Using Nitro as a [Vite plugin](/docs/vite)? Prefer the Vite CLI (`vite dev`, `vite build`, `vite preview`); the Nitro dev server does not support the Vite builder.
::



## `nitro dev`

Start the development server with hot reloading.

```sh
npx nitro dev [--dir <dir>] [--port <port>] [--host <host>]
```

| Flag | Description |
| --- | --- |
| `--dir` | Project root directory. |
| `--port` | Port to listen on. |
| `--host` | Hostname to listen on. |

The dev server watches your Nitro config: changes to `runtimeConfig` or `routeRules` are hot reloaded, while other config changes trigger a full dev server restart.

**Example:**

```sh
npx nitro dev --port 4000 --host 0.0.0.0
```

## `nitro build`

Build the project for production. Nitro prepares the build directory, copies public assets, prerenders any configured routes, and bundles the server into the output directory (`.output/` by default).

```sh
npx nitro build [--dir <dir>] [--preset <preset>] [--minify] [--builder <builder>] [--compatibility-date <date>]
```

| Flag | Description |
| --- | --- |
| `--dir` | Project root directory. |
| `--preset` | The deployment [preset](/deploy) to build for (you can also use the `NITRO_PRESET` environment variable). |
| `--minify` | Minify the output, overriding preset defaults. Use `--no-minify` to disable minification. |
| `--builder` | The bundler to use: `rollup`, `rolldown`, or `vite` (you can also use the `NITRO_BUILDER` environment variable). |
| `--compatibility-date` | The date to use for preset compatibility (you can also use the `NITRO_COMPATIBILITY_DATE` environment variable). |

**Example:**

```sh
npx nitro build --preset cloudflare_module
```

### `NITRO_PRESET` vs `--preset`

Both set the deployment preset, and the `--preset` flag takes precedence over the `NITRO_PRESET` environment variable. The environment variable is handy on deployment platforms and in CI, where you often cannot change the build command:

```sh
NITRO_PRESET=cloudflare_module npm run build
```

## `nitro preview`

Start a local server to preview the production build output.

```sh
npx nitro preview [--dir <dir>] [--port <port>] [--host <host>]
```

| Flag | Description |
| --- | --- |
| `--dir` | Project root directory. |
| `--port` | Port to listen on. |
| `--host` | Hostname to listen on. |

**Example:**

```sh
npx nitro build
npx nitro preview
```

## `nitro serve`

Start the server from the project sources, without a build step (alias: `nitro start`).

```sh
npx nitro serve [--dir <dir>] [--port <port>] [--host <host>] [--dev]
```

| Flag | Description |
| --- | --- |
| `--dir` | Project root directory. |
| `--port` | Port to listen on (defaults to the `NITRO_PORT` or `PORT` environment variable, then `3000`). |
| `--host` | Hostname to listen on (defaults to the `NITRO_HOST` or `HOST` environment variable). |
| `--dev` | Start in development mode, like `nitro dev` without a builder. |

The server runs in production mode without a builder ([`builder: false`](/config#builder)), whatever builder is configured: your files run directly in the server process, and public assets are served from their source directories. With [`buildPlugins`](/config#buildplugins), whose hooks may be async, they run in a Node.js worker that listens on the port itself instead. WebSocket handlers and scheduled tasks are supported. Environment variables are loaded from `.env.production` and `.env` files, without overriding existing ones.

On `SIGINT` or `SIGTERM`, the server stops accepting connections, waits for in-flight requests and runs the `close` hooks before exiting. If the server (or its worker) exits unexpectedly, the command exits with code `1` so a process manager can restart it.

::warning
Running without a builder is experimental. Bundler options and plugins added with the `build:config` hook do not apply, see [`builder`](/config#builder) for the details.
::

**Example:**

```sh
npx nitro start --port 8080
```

## `nitro deploy`

Build the project, then run the deploy command of the selected preset.

```sh
npx nitro deploy [--dir <dir>] [--prebuilt] [build flags...] [-- <extra deploy args>]
```

| Flag | Description |
| --- | --- |
| `--prebuilt` | Skip the build step and deploy the existing build output. |

All [`nitro build`](#nitro-build) flags (`--preset`, `--minify`, `--builder`, `--compatibility-date`, ...) are also accepted and applied to the build step. Any arguments after `--` are appended to the underlying deploy command (or passed as `args` to a function deploy command).

::note
Not all presets support `nitro deploy`. If the selected preset does not define a deploy command (or no build output is found), the command fails with an error. In that case, use a different preset, configure a deploy command in your Nitro config, or deploy manually following the [deployment guide](/deploy) of your provider.
::

**Example:**

```sh
# Build with the cloudflare_module preset, then run wrangler deploy
npx nitro deploy --preset cloudflare_module

# Deploy an existing build without rebuilding
npx nitro deploy --prebuilt
```

## `nitro task`

Operate on [Nitro tasks](/docs/tasks) (experimental).

::important
Tasks support is experimental and requires the `experimental.tasks` flag. The `task` subcommands work against the **currently running dev server**, so run them in a second terminal while `nitro dev` is running. See the [tasks documentation](/docs/tasks) for details.
::

### `nitro task list`

List available tasks with their descriptions.

```sh
npx nitro task list [--dir <dir>]
```

| Flag | Description |
| --- | --- |
| `--dir` | Project root directory. |

### `nitro task run`

Run a task by name, optionally with a JSON payload.

```sh
npx nitro task run <name> [--dir <dir>] [--payload <json>]
```

| Flag | Description |
| --- | --- |
| `--dir` | Project root directory. |
| `--payload` | JSON string parsed and passed to the task as its payload. |

**Example:**

```sh
npx nitro task run db:migrate --payload '{"force": true}'
```

## `nitro docs`

Open the Nitro documentation ([nitro.build](https://nitro.build)) in your browser, optionally at a specific page path (e.g. `/docs/routing`).

```sh
npx nitro docs [page]
```

---

### 1.Docs 61.Vite (`docs/1.docs/61.vite.md`)

---
icon: i-simple-icons-vite
navigation:
  title: Vite
---

# Vite Integration

> Use Nitro as a Vite plugin to build full-stack applications, and choose the builder that bundles your server.

Nitro integrates with [Vite](https://vite.dev) as a plugin. Adding it to a Vite project gives you a full server alongside your frontend: API routes, server-side rendering, and a production build that deploys anywhere.

## Nitro as a Vite plugin

Add the plugin to your Vite config:

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

export default defineConfig({
  plugins: [nitro()],
});
```

With the plugin enabled, you use the Vite CLI for everything:

- **`vite dev`**: One dev server for both frontend and backend. Nitro serves your server routes and assets while Vite handles client modules. Server code runs in an isolated worker and automatically reloads when server files change, while client code keeps Vite's hot module replacement (HMR).
- **`vite build`**: Builds the client and the server together into a single deployable `.output/` directory, using any [deployment preset](/deploy).
- **`vite preview`**: Serves the production build locally through Nitro, including static assets and WebSocket support.

::tip
Follow the [quick start](/docs/quick-start#add-to-a-vite-project) for a step-by-step guide to adding Nitro to an existing Vite project.
::

::note
When using the Vite plugin, prefer `vite dev` over `nitro dev`; the Nitro CLI dev server does not support the Vite builder. `nitro build` works and runs the Vite build under the hood.
::

## Frontend frameworks

The Nitro plugin composes with other Vite plugins, so you can use your favorite frontend framework for server-side rendering with client hydration:

```ts [vite.config.ts]
import { defineConfig } from "vite";
import { nitro } from "nitro/vite";
import react from "@vitejs/plugin-react";

export default defineConfig({
  plugins: [nitro(), react()],
});
```

Nitro automatically detects a file named `entry-server.(ts|js|tsx|jsx|mjs)` (in the project root, `app/`, `src/`, or the server directory) and uses it as the SSR entry.

Instead of re-explaining each setup, see the working examples:

- [SSR with React](/examples/vite-ssr-react)
- [SSR with Vue Router](/examples/vite-ssr-vue-router)
- [SSR with Solid](/examples/vite-ssr-solid)
- [SSR with Preact](/examples/vite-ssr-preact)
- [React Server Components](/examples/vite-rsc)
- [SSR with plain HTML](/examples/vite-ssr-html)

::read-more{to="/docs/renderer"}
Learn more about how unmatched routes are rendered in the [renderer](/docs/renderer) and [server entry](/docs/server-entry) guides.
::

## Configuring Nitro with Vite

Nitro configuration can live in three places, all accepting the same [options](/config):

1. A `nitro.config.ts` file (recommended for most projects)
2. The `nitro` key in your Vite config
3. Inline plugin options passed to `nitro()`

::code-group
```ts [nitro.config.ts]
import { defineConfig } from "nitro";

export default defineConfig({
  serverDir: "./server",
});
```
```ts [vite.config.ts (nitro key)]
import { defineConfig } from "vite";
import { nitro } from "nitro/vite";

export default defineConfig({
  plugins: [nitro()],
  nitro: {
    serverDir: "./server",
  },
});
```
```ts [vite.config.ts (inline)]
import { defineConfig } from "vite";
import { nitro } from "nitro/vite";

export default defineConfig({
  plugins: [
    nitro({
      serverDir: "./server",
    }),
  ],
});
```
::

Inline plugin options take precedence over the `nitro` key in the Vite config, and both override `nitro.config.ts`. The `dev` mode and `rootDir` are inferred from Vite and cannot be overridden.

Vite plugins can also extend Nitro programmatically by exposing a `nitro` key (a [Nitro module](/docs/modules)) on the plugin object. See the [Vite Nitro plugin example](/examples/vite-nitro-plugin).

### Using a specific Vite package

Nitro does not depend on `vite` itself: it resolves the one installed in your project. A framework running Vite programmatically should pass its own `vite` package with the `vite.path` option, so the dev module runner is created from the Vite instance that is actually running (in a monorepo, a different version can be hoisted next to the app):

```ts
import { createServer } from "vite";
import { nitro } from "nitro/vite";

const server = await createServer({
  plugins: [nitro({ vite: { path: import.meta.resolve("vite") } })],
});
```

The option accepts a path or `file://` URL to the `vite` package directory or entry.

### Vite options

Vite-specific options are set under the `vite` key. Like other options, they can be passed to the plugin, set under `nitro` in the Vite config, or in `nitro.config.ts`:

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

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

::field{name="services" type="Record<string, { entry: string }>"}
Register additional server environments as fetchable services. Server environments with an entry (such as `ssr`) are registered automatically.
::

::field{name="assetsImport" type="boolean"}
Enable `?assets` imports for collecting CSS and JS assets from entry points (default: `true`).
::

::field{name="serverReload" type="boolean"}
Reload the server (and browser) when a server-only module changes (default: `true`).
::

## Testing with Vitest

[Vitest](https://vitest.dev) picks up the `nitro()` plugin from your Vite config and runs your tests against the Nitro runtime. See the [testing guide](/docs/testing) for setup and details.

## Builders

Nitro can bundle your server with one of three builders:

| Builder | Description |
| --- | --- |
| `rolldown` | Default. Rust-based bundler. Requires `rolldown` to be installed (installed on demand when building). |
| `rollup` | Battle-tested bundler with a mature plugin ecosystem. Requires `rollup` to be installed. |
| `vite` | Full-stack builds via the Vite plugin. Requires `vite` to be installed. |

### How the builder is selected

The builder is resolved in this order:

1. The `builder` option in your Nitro config.
2. The `NITRO_BUILDER` environment variable.
3. Auto-detection: if `vite` is installed and your `vite.config` uses the `nitro()` plugin, the `vite` builder is used.
4. Otherwise, Nitro defaults to `rolldown`. If `rolldown` is not installed (and no `rollupConfig` or `rolldownConfig` is set), `nitro dev` runs the server sources without a builder ([`builder: false`](/config#builder)) and `nitro build` offers to install it.

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

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

Or without touching the config file:

```bash
NITRO_BUILDER=rollup nitro build
```

If you explicitly select `rollup` or `vite` and the package is not installed, Nitro prompts to install it as a dev dependency. `rolldown` is installed the same way when building.

The `vite` builder prerenders routes with `rolldown` (resolved from your project or your `vite`). Without it (e.g. with Vite 7), the prerenderer runs the server sources without a builder.

### Which builder to pick

- Stick with **`rolldown`** for standalone servers and APIs. It is the fastest option.
- Pick **`rollup`** if you depend on Rollup-specific plugins or need maximum ecosystem compatibility.
- Use **`vite`** (usually via auto-detection) whenever you have a frontend built with Vite. It is the only builder that bundles client and server together.

### Customizing the bundler

To add build plugins for every builder (including `vite`, and `builder: false`), use [`buildPlugins`](/config#buildplugins). With `vite`, they apply to the server bundle (`nitro` environment) as bundler plugins, after Vite's own plugins; use Vite's `plugins` for Vite plugins.

```ts [nitro.config.ts]
import { defineConfig } from "nitro";
import myPlugin from "./my-plugin";

export default defineConfig({
  buildPlugins: [myPlugin()],
});
```

For advanced cases, you can pass extra configuration directly to the underlying bundler with `rollupConfig` or `rolldownConfig`:

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

export default defineConfig({
  rolldownConfig: {
    // Extra Rolldown options and plugins
  },
  rollupConfig: {
    // Extra Rollup options and plugins
  },
});
```

Each option applies to its matching builder. With the `vite` builder, both are merged into the server bundle configuration depending on whether Vite runs on Rolldown (`rolldown-vite`) or Rollup.

::warning
These options are escape hatches. Prefer built-in Nitro options when available, as low-level bundler overrides can break across builders.
::

## Good to know

- **Server reloads instead of HMR**: changes to server-only modules trigger a server reload (and a browser refresh) rather than hot module replacement. Modules shared between client and server keep normal Vite HMR.
- **Dev server port**: the port is resolved from the `PORT` environment variable, then Vite's `server.port`, then Nitro's `devServer.port` (default: `3000`).
- **Environment files**: in both dev and build, Nitro loads `.env`, `.env.local`, and mode-specific variants (`.env.[mode]`, `.env.[mode].local`) following [Vite conventions](https://vite.dev/guide/env-and-mode#env-files). Existing environment variables take precedence over `.env` values.
- **Vite environments**: Nitro uses the [Vite environment API](https://vite.dev/guide/api-environment) and registers `client` and `nitro` environments (plus `ssr` when an SSR entry exists). You can configure the client entry point via `environments.client.build.rollupOptions.input` in your Vite config, as shown in the [SSR examples](/examples/vite-ssr-react).

---

### 1.Docs 62.Utils (`docs/1.docs/62.utils.md`)

---
icon: i-lucide-braces
navigation:
  title: Utils
---

# Server Utilities

> A reference of everything you can import from `nitro` and its subpaths in your server code.

Nitro has **no auto-imports**: every utility must be explicitly imported from the main `nitro` entry or one of its subpaths. This page maps each export to its import path.

## Quick reference

| What you want to do | Import |
| --- | --- |
| Define an event handler | `import { defineHandler } from "nitro"`{lang=ts} |
| Define a middleware | `import { defineMiddleware } from "nitro"`{lang=ts} |
| Throw an HTTP error | `import { HTTPError } from "nitro"`{lang=ts} |
| Read the body, query, cookies, ... | `import { readBody, getQuery } from "nitro/h3"`{lang=ts} |
| Call your own routes internally | `import { serverFetch } from "nitro"`{lang=ts} |
| Handle WebSocket connections | `import { defineWebSocketHandler } from "nitro"`{lang=ts} |
| Add a runtime plugin | `import { definePlugin } from "nitro"`{lang=ts} |
| Customize the error response | `import { defineErrorHandler } from "nitro"`{lang=ts} |
| Cache a handler or function | `import { defineCachedHandler, defineCachedFunction } from "nitro/cache"`{lang=ts} |
| Read/write key-value storage | `import { useKV } from "nitro/kv"`{lang=ts} |
| Query a SQL database | `import { useDatabase } from "nitro/database"`{lang=ts} |
| Access runtime config | `import { useRuntimeConfig } from "nitro/runtime-config"`{lang=ts} |
| Define and run tasks | `import { defineTask, runTask } from "nitro/task"`{lang=ts} |
| Access the app instance and hooks | `import { useNitroApp, useNitroHooks } from "nitro/app"`{lang=ts} |
| Configure Nitro (`nitro.config.ts`) | `import { defineConfig } from "nitro"`{lang=ts} |
| Use Nitro as a Vite plugin | `import { nitro } from "nitro/vite"`{lang=ts} |

## `nitro`

The main entry contains everything you need for day-to-day route handlers.

| Export | Description |
| --- | --- |
| `defineHandler` | Define an event handler for a [route or middleware file](/docs/routing). |
| `defineMiddleware` | Define a [middleware](/docs/routing#middleware) that runs before route handlers. |
| `defineWebSocketHandler` | Define a [WebSocket handler](/docs/websocket) for a route file. |
| `definePlugin` | Define a [runtime plugin](/docs/plugins) that runs on server startup. |
| `defineErrorHandler` | Define a custom [error handler](/config#errorhandler) to replace the built-in error page. |
| `defineConfig` | Define Nitro [configuration](/docs/configuration) (used in `nitro.config.ts`, not in runtime code). |
| `defineRouteMeta` | Attach route metadata such as [OpenAPI specs](/docs/openapi) (build-time macro). |
| `HTTPError` | Throw HTTP errors with a status code: `throw new HTTPError("Not found", { status: 404 })`{lang=ts}. |
| `HTTPResponse` | Return a body with a custom status and headers from a handler. |
| `html` | Tagged template literal returning an HTML response (`text/html` content type). |
| `serverFetch` | Call your own routes internally, without a network round-trip. |
| `fetch` | Like global `fetch`, but paths starting with `/` are routed to your own server. |

Commonly used types are also exported: `H3Event`, `EventHandlerRequest`, and `EventHandlerWithFetch`.

```ts [server/routes/hello.ts]
import { defineHandler, HTTPError } from "nitro";

export default defineHandler((event) => {
  const name = event.url.searchParams.get("name");
  if (!name) {
    throw new HTTPError("Missing name", { status: 400 });
  }
  return { hello: name };
});
```

### Internal fetch

`serverFetch` calls a route of your own app directly: the request goes through the Nitro app (route rules, middleware, plugins) without touching the network:

```ts [server/routes/summary.ts]
import { defineHandler, serverFetch } from "nitro";

export default defineHandler(async () => {
  const res = await serverFetch("/api/stats"); // returns a web Response
  return { stats: await res.json() };
});
```

The `fetch` export is a universal variant: absolute paths (starting with `/`) are handled internally via `serverFetch`, everything else falls back to global `fetch`:

```ts
import { fetch } from "nitro";

await fetch("/api/hello"); // handled by your own server
await fetch("https://example.com"); // regular network fetch
```

### Error handler

Point the [`errorHandler` config](/config#errorhandler) to a file that exports a custom error handler:

```ts [error.ts]
import { defineErrorHandler } from "nitro";

export default defineErrorHandler((error, event) => {
  return new Response(`[custom error] ${error.message}`, {
    status: error.status,
    headers: { "Content-Type": "text/plain" },
  });
});
```

### Runtime plugins

Plugins run once on server startup and can hook into the [runtime lifecycle](/docs/lifecycle):

```ts [server/plugins/log.ts]
import { definePlugin } from "nitro";

export default definePlugin((nitroApp) => {
  nitroApp.hooks.hook("request", (event) => {
    console.log("request:", event.url.pathname);
  });
});
```

See the [plugins guide](/docs/plugins) for more.

## `nitro/h3`

Re-exports all [H3 v2](https://h3.dev) utilities: `readBody`, `getQuery`, `getCookie`, `setCookie`, `proxy`, `redirect`, and many more.

```ts [server/routes/form.post.ts]
import { defineHandler } from "nitro";
import { readBody, getCookie } from "nitro/h3";

export default defineHandler(async (event) => {
  const body = await readBody(event);
  const session = getCookie(event, "session");
  return { body, session };
});
```

::note
See the [H3 utilities documentation](https://h3.dev/utils) for the full list.
::

## `nitro/cache`

| Export | Description |
| --- | --- |
| `defineCachedHandler` | Wrap an event handler with caching. |
| `defineCachedFunction` | Cache the result of any (async) function. |

```ts
import { defineCachedHandler } from "nitro/cache";

export default defineCachedHandler(() => `Generated at ${new Date().toISOString()}`, {
  maxAge: 60,
});
```

See the [cache guide](/docs/cache) for options, cache keys, and invalidation.

## `nitro/kv`

| Export | Description |
| --- | --- |
| `useKV` | Access the key-value [storage layer](/docs/storage), optionally scoped to a base (e.g. `useKV("data")`{lang=ts}). |

```ts
import { useKV } from "nitro/kv";

await useKV("data").set("visits", 42);
```

See the [KV storage guide](/docs/storage) for mountpoints and drivers.

::note
`useStorage` from `nitro/storage` still works as a deprecated alias of `useKV`.
::

## `nitro/database`

| Export | Description |
| --- | --- |
| `useDatabase` | Access a configured SQL [database](/docs/database) connection (default: `useDatabase()`{lang=ts}, named: `useDatabase("users")`{lang=ts}). |

```ts
import { useDatabase } from "nitro/database";

const db = useDatabase();
const { rows } = await db.sql`SELECT * FROM users`;
```

See the [database guide](/docs/database) for connectors and configuration.

## `nitro/runtime-config`

| Export | Description |
| --- | --- |
| `useRuntimeConfig` | Access [runtime configuration](/docs/configuration#runtime-configuration), overridable with `NITRO_*` environment variables. |

```ts
import { useRuntimeConfig } from "nitro/runtime-config";

const { apiToken } = useRuntimeConfig();
```

## `nitro/task`

| Export | Description |
| --- | --- |
| `defineTask` | Define a [task](/docs/tasks) in a `server/tasks/` file. |
| `runTask` | Run a task by name from anywhere in your server code. |

```ts
import { runTask } from "nitro/task";

const { result } = await runTask("db:migrate", { payload: { force: true } });
```

See the [tasks guide](/docs/tasks). Tasks are experimental and require the `experimental.tasks` flag.

## `nitro/app`

Lower-level access to the running Nitro app.

| Export | Description |
| --- | --- |
| `useNitroApp` | Access the current [Nitro app instance](/docs/lifecycle) (`fetch`, `hooks`, ...). |
| `useNitroHooks` | Access the [runtime hooks](/docs/plugins#nitro-runtime-hooks) instance to register hooks outside of plugins. |
| `getRouteRules` | Get the resolved [route rules](/docs/routing#route-rules) for a method and pathname: `getRouteRules("GET", "/blog/post")`{lang=ts}. |
| `serverFetch`, `fetch` | Same as the [main entry exports](#internal-fetch). |

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

useNitroHooks().hook("error", (error) => {
  console.error(error);
});
```

## `nitro/context`

| Export | Description |
| --- | --- |
| `useRequest` | Access the current request from anywhere within the request lifecycle, without passing `event` around. |

```ts
import { useRequest } from "nitro/context";

const request = useRequest();
```

::warning
`useRequest` is experimental: it requires the `experimental.asyncContext: true`{lang=ts} config flag and only works on runtimes with `AsyncLocalStorage` support (Node.js and compatible runtimes).
::

## `nitro/config`

| Export | Description |
| --- | --- |
| `defineConfig` | Same as the `defineConfig` export from the main `nitro` entry (also exported as the deprecated `defineNitroConfig` alias). |

Prefer importing `defineConfig` from `"nitro"` directly.

## `nitro/types`

Type-only entry with all public Nitro types, including `NitroConfig`, `Nitro`, `NitroApp`, `NitroAppPlugin`, `NitroErrorHandler`, `NitroRuntimeConfig`, `NitroRuntimeHooks`, and `Task`.

```ts
import type { NitroRuntimeConfig } from "nitro/types";
```

## `nitro/vite`

| Export | Description |
| --- | --- |
| `nitro` | The Nitro [Vite plugin](/docs/vite). |

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

export default defineConfig({
  plugins: [nitro()],
});
```

The `NitroPluginConfig` and `ViteServiceConfig` types are also exported. See the [Vite integration guide](/docs/vite).

## `nitro/builder`

Programmatic API for tools that drive Nitro without the CLI (build wrappers, framework integrations).

| Export | Description |
| --- | --- |
| `createNitro` | Create a Nitro instance from an inline config (and `nitro.config` in `rootDir`). |
| `loadOptions` | Load and resolve Nitro options without creating an instance. |
| `prepare` | Clean the output directories. |
| `copyPublicAssets` | Copy public assets to the output directory. |
| `prerender` | Prerender routes. |
| `build` | Build the server bundle. |
| `createDevServer` | Create a development server for a Nitro instance (returns `{ fetch, upgrade, listen, reload, close }`). |
| `getBuildInfo` | Read `nitro.json` of the last build (accepts `{ rootDir, outputDir }`). |
| `startPreview` | Start an in-process preview of a build (`{ rootDir, outputDir?, loader? }` returns `{ fetch, upgrade?, close }`). |
| `deploy` | Run the deploy command of the preset for an existing build (`deploy(nitro, { args? })`). |

```ts
import { build, copyPublicAssets, createNitro, deploy, prepare, prerender } from "nitro/builder";

const nitro = await createNitro({ rootDir: ".", dev: false, preset: "cloudflare_module" });
await prepare(nitro);
await copyPublicAssets(nitro);
await prerender(nitro);
await build(nitro);
await deploy(nitro, { args: ["--env", "staging"] });
await nitro.close();
```

`deploy` uses `commands.deploy` from the resolved options. It is either a shell command (run from `rootDir`, with `./` paths rewritten to the output directory) or a function for presets that deploy programmatically (such as `zephyr`). Presets that lack a deploy command throw an error.

## Other entries

The remaining subpaths (`nitro/meta` for the package `version`, `runtimeDir` and `runtimeDependencies`, `nitro/vite/runtime`, `nitro/tsconfig`) are internal and rarely needed in application code.

---

### 1.Docs 63.Typescript (`docs/1.docs/63.typescript.md`)

---
icon: i-simple-icons-typescript
navigation:
  title: TypeScript
---

# TypeScript

> Nitro is TypeScript-first: write your config, handlers, and server code in TypeScript with zero setup.

Nitro is written in TypeScript and bundles your server code, so `.ts` files work out of the box in development and production, with no separate compile step. The TypeScript compiler is only needed for type checking (for example `tsc --noEmit` in CI).

All public utilities ship with types:

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

export default defineConfig({
  // Fully typed config with completions
});
```

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

export default defineHandler((event) => {
  return { hello: "world" }; // Typed event and inferred return type
});
```

Types (such as `NitroConfig`, `NitroModule`, or `NitroRuntimeConfig`) are exported from `nitro/types`:

```ts
import type { NitroModule } from "nitro/types";
```

## `tsconfig.json` setup

Nitro provides a shareable TypeScript configuration you can extend from:

```json [tsconfig.json]
{
  "extends": "nitro/tsconfig"
}
```

It enables modern, strict defaults matching how Nitro bundles your code: `strict` type checking, bundler module resolution, `noEmit`, `verbatimModuleSyntax`, and `allowImportingTsExtensions` (so you can use explicit `.ts` extensions in imports). You can override any option by adding your own `compilerOptions` on top:

```json [tsconfig.json]
{
  "extends": "nitro/tsconfig",
  "compilerOptions": {
    "jsx": "react-jsx"
  }
}
```

## `typescript` config options

| Option | Default | Description |
| --- | --- | --- |
| `tsConfig` | Project `tsconfig.json` | TypeScript config used by the bundler for JSX options and path aliases. |
| `tsconfigPaths` | `true` | Resolve path aliases (`compilerOptions.paths`) of `tsConfig` for the server sources. |

Nitro reads your project's root `tsconfig.json` to pick up JSX settings (`jsx`, `jsxFactory`, `jsxFragmentFactory`, `jsxImportSource`) and path aliases (`compilerOptions.paths`). Set `typescript.tsConfig` to override it:

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

export default defineConfig({
  typescript: {
    tsConfig: {
      compilerOptions: {
        jsx: "react-jsx",
      },
    },
  },
});
```

### Path aliases

Path aliases (`compilerOptions.paths`) are resolved for your server sources with every builder and with `builder: false`, including those inherited with `extends`.

- The [`alias`](/config#alias) option (and Vite's `resolve.alias`) takes precedence over path aliases.
- Paths to declaration files (`.d.ts`) are ignored, the import keeps its regular resolution.
- With the Vite builder, they apply to the Nitro environment only. Enable Vite's [`resolve.tsconfigPaths`](https://vite.dev/config/shared-options#resolve-tsconfigpaths) option for the other environments. In development, patterns starting with `*` do not apply to installed packages.
- Set `typescript.tsconfigPaths: false` to disable them.

---

### 1.Docs 64.Testing (`docs/1.docs/64.testing.md`)

---
icon: i-simple-icons-vitest
navigation:
  title: Testing
---

# Testing

> Test your server routes, plugins and runtime utilities with Vitest.

[Vitest](https://vitest.dev) picks up the `nitro()` plugin from your Vite config. Test files run in the `nitro` Vite environment, so `nitro/*` runtime utilities, routes, plugins and runtime config work the same as in your server code.

## Setup

Install `vitest` as a dev dependency:

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

No extra config is needed, as long as `vite.config.ts` (or `vitest.config.ts`) registers the `nitro()` plugin. A project with only a `nitro.config.ts` needs to add one:

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

export default defineConfig({
  plugins: [nitro()],
});
```

Then run `vitest`.

## Writing tests

Use `serverFetch` to send requests to your Nitro app, and call runtime utilities directly:

```ts [test/app.test.ts]
import { expect, test } from "vitest";
import { serverFetch } from "nitro";
import { useKV } from "nitro/kv";

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

test("kv", async () => {
  await useKV().setItem("foo", "bar");
  expect(await useKV().getItem("foo")).toBe("bar");
});
```

Tests run in Vitest workers, without starting the Nitro dev server. With Vitest's default isolation, each test file gets its own Nitro app instance, and its `close` hooks run when the file finishes.

`vi.mock` works as usual, including for modules imported by your routes and [Nitro plugins](/docs/plugins): the app is created after the test file's mocks are registered.

::read-more{to="/examples/vitest"}
See the [Vitest example](/examples/vitest) for a complete project.
::

## Watch mode

In watch mode, editing a route, middleware or plugin reruns the tests that use the app. Adding or removing one rescans the server directories first, and changing the Nitro config restarts Vitest.

## SSR entry

Pages rendered by an [SSR entry](/docs/vite#frontend-frameworks) (`entry-server.ts`) work with `serverFetch` too. In tests, service entries such as `ssr` are loaded in the `nitro` environment rather than their own environment, so plugins and options that only apply to that environment are not used.

Frameworks that rely on several server environments (such as React Server Components) are not supported yet.

## Choosing the environment

The `nitro` test environment is used when `test.environment` is not set. To use another environment, set `test.environment` and opt individual files back in with a `// @vitest-environment nitro` comment, or the other way around.

Projects defined inline in `test.projects` don't get the default environment. Set `environment: "nitro"` in each project that runs server tests:

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

export default defineConfig({
  plugins: [nitro()],
  test: {
    projects: [{ extends: true, test: { name: "server", environment: "nitro" } }],
  },
});
```

## Limitations

- The `vmThreads` and `vmForks` pools don't support the `nitro` environment.
- Tests always run in Node.js: the [`devServer.runner`](/config#devserver) option, including the one set by a preset (such as `miniflare` for Cloudflare), is not used, so platform modules and bindings (such as `cloudflare:workers`) are not available.

---

### 1.Docs 7.Cache (`docs/1.docs/7.cache.md`)

---
icon: ri:speed-line
---

# Cache

> Nitro provides a caching system powered by [ocache](https://ocache.unjs.io/), with pluggable [cache storage](#cache-storage) (in-memory, filesystem or the [KV storage layer](/docs/storage)).

Caching lets you store the result of expensive operations (rendered responses, upstream API calls, heavy computations) and serve it again without redoing the work. Nitro offers three ways in:

- [Cached handlers](#cached-handlers): cache the full response of an event handler.
- [Cached functions](#cached-functions): cache the result of any function and reuse it across handlers.
- [Route rules](#using-route-rules): enable caching for route patterns from your config, without touching handler code.

## Cached handlers

To cache an event handler, use the `defineCachedHandler` method.

It works like `defineHandler` but with a second parameter for the [cache options](#options).

```ts [routes/cached.ts]
import { defineCachedHandler } from "nitro/cache";

export default defineCachedHandler((event) => {
  return "I am cached for an hour";
}, { maxAge: 60 * 60 });
```

With this example, the response will be cached for 1 hour. Once the cache expires, the next request waits for the handler to resolve a fresh value before responding. If you prefer to immediately serve the stale response while it is revalidated in the background, set `swr: true` (see [SWR behavior](#swr-behavior)).

See the [options](#options) section for more details about the available options.

::important
`maxAge` defaults to **1 second**. Always set it explicitly for anything you actually want cached.
::

### What the handler can see

For cacheable requests, **a handler receives exactly the request data that the cache key covers**. Everything else is removed before the handler runs, so a handler cannot render output from a value the key does not distinguish and have it stored under a shared key:

- **Request headers** are stripped unless listed in [`varies`](#handler-only-options). This includes `if-none-match` / `if-modified-since` (Nitro answers conditional requests itself) and tracing headers such as `traceparent` or `x-request-id`.
- **Query parameters** are stripped unless listed in [`allowQuery`](#handler-only-options). By default `/page`, `/page?a=1` and `/page?utm=x` all share one entry, and `url.searchParams` is empty inside the handler.
- **Cookies** are stripped unless listed in [`allowCookies`](#handler-only-options), and `set-cookie` is removed from every cached response.
- **`authorization` / `proxy-authorization`** are stripped unless [`allowAuthorization`](#handler-only-options) is set.
- **`host`** is the one header that is rewritten rather than removed: it carries the host of the resolved request origin, which is part of the key. A handler serving several hostnames therefore gets one entry per host.

Use [`shouldBypassCache`](#shared-options) for requests that must reach the handler untouched. A bypassed request keeps its headers, cookies, query string and body, and its response is returned without being stored.

::read-more{to="https://ocache.unjs.io/docs/handler#headers-the-handler-cant-see"}
ocache: headers the handler can't see.
::

### Automatic HTTP headers

When using `defineCachedHandler`, Nitro automatically manages HTTP cache headers on cached responses:

- **`etag`**: a weak ETag (`W/"..."`) is generated from the response body hash if not already set by the handler.
- **`cache-control`**: synthesized from the lifetime actually enforced for the entry, unless the handler already set one (see the table below).
- **`vary`**: every name in `varies` is merged into the response `Vary`, plus `Cookie` when `allowCookies` is used. `allowQuery` adds nothing, because query parameters already live in the URL.
- **`x-cache`**: a CDN-style cache status header (`HIT`, `STALE`, `REVALIDATED` or `MISS`). Customizable with the [`cacheStatusHeader`](#handler-only-options) option.

| Options | Synthesized `cache-control` |
| ------- | --------------------------- |
| `{ maxAge: 60 }` | `max-age=60` |
| `{ maxAge: 60, swr: true }` | `max-age=60, s-maxage=60` |
| `{ maxAge: 60, swr: true, staleMaxAge: 600 }` | `max-age=60, s-maxage=60, stale-while-revalidate=600` |

`stale-while-revalidate` is only advertised when `staleMaxAge` is set, since the directive requires a number of seconds. With `swr: true` and no `staleMaxAge` the stale window is unlimited, so downstream caches simply revalidate after `max-age` while Nitro answers from its stale copy.

To keep caching server-side only without advertising `cache-control` to clients and CDNs, set [`sendCacheControl: false`](#handler-only-options).

::note
**`last-modified` is never generated.** The time an entry was filled is not the time the content changed, so set the header yourself when the handler knows the real modification time. A handler-set `cache-control` is likewise never overwritten.
::

### Conditional requests (304 Not Modified)

Cached handlers automatically support conditional requests. When a client sends `if-none-match` matching the cached `etag`, or `if-modified-since` at or after a `last-modified` the handler set, Nitro returns a `304 Not Modified` response without a body. `if-none-match` wins when both are present.

The request validators are captured before the request is narrowed, so **the handler never sees them** and cannot do its own conditional handling.

### Cacheable requests

Only `GET` and `HEAD` requests are cached. All other HTTP methods (`POST`, `PUT`, `DELETE`, etc.) automatically bypass the cache and call the handler directly, as do requests carrying a `Range` header.

`GET` and `HEAD` are cached under **separate entries**, since a `HEAD` response carries no body. The method stays part of the key even with a custom `getKey`.

### Request deduplication

When multiple concurrent requests hit the same cache key while the cache is being resolved, only one invocation of the handler runs. All concurrent requests wait for and share the same result.

A shared resolution is bounded by [`maxResolveTime`](#shared-options) (30 seconds by default). On timeout every waiter rejects with a `TimeoutError`, the entry being refreshed is evicted, and the key is free again. Handlers additionally receive that deadline as `event.req.signal`, so you can forward it to upstream `fetch` calls.

## Cached functions

You can also cache a function using `defineCachedFunction`. This is useful for caching the result of a function that is not an event handler, but is part of one, and reusing it in multiple handlers.

For example, you might want to cache the result of an API call for one hour:

```ts [routes/api/stars/[...repo\\].ts]
import { defineCachedFunction } from "nitro/cache";
import { defineHandler, type H3Event } from "nitro";

export default defineHandler(async (event) => {
  const { repo } = event.context.params;
  const stars = await cachedGHStars(repo).catch(() => 0)

  return { repo, stars }
});

const cachedGHStars = defineCachedFunction(async (repo: string) => {
  const data = await fetch(`https://api.github.com/repos/${repo}`).then(res => res.json());

  return data.stargazers_count;
}, {
  maxAge: 60 * 60,
  name: "ghStars",
  getKey: (repo: string) => repo
});
```

The stars will be cached using the [cache storage](#cache-storage) (in-memory by default) under a [cache key](#cache-keys) derived from the group, name and `getKey` result, with `value` being the number of stars.

```json
{"expires":1677851092249,"value":43991,"mtime":1677847492540,"integrity":"ZUHcsxCWEH"}
```

::important
Because the cached data is serialized to JSON, it is important that the cached function does not return anything that cannot be serialized, such as Symbols, Maps, Sets...

A value that **is** bytes (a `Uint8Array`, `ArrayBuffer`, or any typed array) is handled without a hook and always comes back as a `Uint8Array`. How it is *stored* depends on the backend: one that declares `binary` (Nitro's `memory` and `fs` [cache drivers](#cache-storage)) keeps the bytes as-is, while a serializing backend (such as the `kv` driver) stores base64. Bytes nested inside an object never take this path, use [`serialize`/`transform`](#function-only-options) for those shapes.
::

::note
If you are using edge workers to host your application, you should follow the instructions below.

In edge workers, the instance is destroyed after each request. Nitro automatically uses `event.waitUntil` to keep the instance alive while the cache is being updated while the response is sent to the client.

To ensure that your cached functions work as expected in edge workers, **you should always pass the `event` as the first argument to the function using `defineCachedFunction`.**

```ts [routes/api/stars/[...repo\\].ts] {5,10,17}
import { defineCachedFunction } from "nitro/cache";
import { defineHandler, type H3Event } from "nitro";

export default defineHandler(async (event) => {
  const { repo } = event.context.params;
  const stars = await cachedGHStars(event, repo).catch(() => 0)

  return { repo, stars }
});

const cachedGHStars = defineCachedFunction(async (event: H3Event, repo: string) => {
  const data = await fetch(`https://api.github.com/repos/${repo}`).then(res => res.json());

  return data.stargazers_count;
}, {
  maxAge: 60 * 60,
  name: "ghStars",
  getKey: (event: H3Event, repo: string) => repo
});
```

This way, the function will be able to keep the instance alive while the cache is being updated without slowing down the response to the client.

On platforms where the request carries no `waitUntil`, pass the platform hook through the [`waitUntil` option](#shared-options) instead.
::

## Options

The `defineCachedHandler` and `defineCachedFunction` functions accept the following options:

### Shared options

These options are available for both `defineCachedHandler` and `defineCachedFunction`:

::field-group
  ::field{name="base" type="string | string[]"}
    Storage base (prefix) used for cache keys, usually matching a storage mount point. :br
    Defaults to `/cache` (stored under the `cache:` prefix). When an array is provided, reads try each base in order and writes go to all of them (multi-tier caching).
  ::
  ::field{name="name" type="string"}
    Guessed from the function name if not provided, and falls back to `anon_<hash>` (computed from the function code) otherwise.
  ::
  ::field{name="group" type="string"}
    Defaults to `'nitro/handlers'` for handlers and `'nitro/functions'` for functions.
  ::
  ::field{name="getKey()" type="(...args) => string"}
    A function that accepts the same arguments as the original function and returns a cache key (`String`). :br
    If not provided, a built-in hash function will be used to generate a key based on the function arguments. For cached handlers, the key is derived from the request origin, path and method. :br :br
    A custom `getKey` replaces the generated key but **not** the request narrowing: `allowQuery`, `allowCookies` and `varies` still decide what the handler sees.
  ::
  ::field{name="integrity" type="string"}
    A value that invalidates the cache when changed. :br
    By default, it is computed from **function code and options**, so changing either silently ignores older entries instead of serving them.
  ::
  ::field{name="maxAge" type="number"}
    Maximum age that cache is valid, in seconds. :br
    Defaults to `1` (second). An explicit `0` disables caching for that handler or function; an omitted or `undefined` value falls back to the default.
  ::
  ::field{name="staleMaxAge" type="number"}
    Maximum number of seconds a stale value can still be served after `maxAge` expires, while revalidation happens in the background. Only applies when `swr` is enabled. :br
    When set to `0`, stale values are never served (equivalent to disabling `swr`). When unset, stale values can be served without a time limit and **no storage TTL is written**, so entries live until the backend evicts them.
  ::
  ::field{name="swr" type="boolean"}
    Enable `stale-while-revalidate` behavior to serve a stale cached value while asynchronously revalidating it. :br
    When enabled, expired cached values are returned immediately while revalidation happens in the background. When disabled, the caller waits while the value is revalidated in the foreground. :br
    Defaults to `false`. See [SWR behavior](#swr-behavior).
  ::
  ::field{name="getMaxAge()" type="(entry: CacheEntry) => number | { maxAge?, staleMaxAge? }"}
    Derive the cache lifetime per entry from the resolved value, overriding the static `maxAge` / `staleMaxAge` options for that entry. Return a number of seconds (shorthand for `maxAge`) or an object to also override `staleMaxAge`. :br
    Useful for values that carry their own expiry, such as access tokens: `getMaxAge: (entry) => entry.value.expires_in`. A resolved value `<= 0` disables caching for that entry. For handlers, `entry.value` is the `Response` and the synthesized `cache-control` follows the returned lifetime.
  ::
  ::field{name="maxResolveTime" type="number"}
    Deadline for one shared resolution, in seconds. Defaults to `30`. :br
    On timeout every waiting caller rejects with a `TimeoutError` and the entry being refreshed is evicted. Set `Infinity` or `0` to disable it, and raise it for legitimately slow resolvers.
  ::
  ::field{name="storage" type="StorageInterface | () => StorageInterface"}
    Override the backend used for this handler or function. :br
    Defaults to Nitro's [cache storage](#cache-storage), configured with the `cache.driver` option.
  ::
  ::field{name="waitUntil()" type="(promise: Promise<any>) => void"}
    Hand background work (cache writes, SWR refreshes, evictions) to the host runtime. :br
    Takes precedence over `event.req.waitUntil`. Only needed on serverless platforms where the request does not carry one.
  ::
  ::field{name="shouldInvalidateCache()" type="(...args) => boolean | Promise<boolean>"}
    A function that returns a `boolean` to invalidate the current cache and create a new one.
  ::
  ::field{name="shouldBypassCache()" type="(...args) => boolean | Promise<boolean>"}
    A function that returns a `boolean` to bypass the current cache without invalidating the existing entry. :br
    For handlers, a bypassed request reaches the handler **unchanged** (credentials, cookies and full query string included) and its response is neither stored nor given cache headers.
  ::
  ::field{name="onError()" type="(error: unknown) => void"}
    A custom error handler called when the cached function throws. :br
    By default, errors are logged to the console and captured by the Nitro error handler.
  ::
::

### Handler-only options

These options are only available for `defineCachedHandler`:

::field-group
  ::field{name="headersOnly" type="boolean"}
    When `true`, skip full response caching and only handle conditional request headers (`if-none-match`, `if-modified-since`) for `304 Not Modified` responses. The handler is called on every request and its own `etag` / `last-modified` are the conditions, so it must set them itself. No `x-cache` header is emitted in this mode.
  ::
  ::field{name="varies" type="string[]"}
    An array of request header names to vary the cache key on. Headers listed here are preserved on the request during cache resolution and included in the cache key, making the cache unique per combination of header values. They are also merged into the response's `Vary` header. :br :br
    Headers **not** listed in `varies` are stripped from the request before calling the handler to ensure consistent cache hits. :br :br
    A response whose own `Vary` names a header outside the key is returned but **not stored**, so keep the two lists in sync. :br :br
    Multi-tenant setups no longer need `['host']` here: the request origin is already part of the generated key. Add `x-forwarded-host` only if your handler reads it behind a proxy.
  ::
  ::field{name="allowQuery" type="boolean | string[]"}
    Allowlist of query parameter names that vary the cache key. **By default no query parameter varies the key or reaches the handler**, so `/page?a=1` and `/page?utm=x` share one entry and `url.searchParams` is empty inside the handler. :br :br
    Set an array to opt specific names back in (case-sensitive, order-independent), or `true` to key on the full query string, which is appropriate for proxies and search endpoints where the parameters are not known ahead of time.
  ::
  ::field{name="allowCookies" type="string[]"}
    Allowlist of cookie names that participate in caching. Listed cookies vary the cache key and are kept in the `cookie` header the handler sees. :br :br
    **By default, no cookies are allowed**: the `cookie` request header is stripped before the handler runs, and any `set-cookie` header is dropped from the response before it is cached or returned, so a per-user cookie (such as a session id) can never leak to another user through the cache. Only allowlist cookies whose values are safe to share across every user hitting the same cache key (e.g. a `theme` or `locale` preference), never per-user secrets. :br :br
    Setting this emits `Vary: Cookie`, which can collapse hit rates in CDNs and shared proxies. Consider moving the choice into the URL with `allowQuery` instead, or pairing it with `sendCacheControl: false`.
  ::
  ::field{name="allowAuthorization" type="boolean"}
    Let `authorization` and `proxy-authorization` reach the handler and vary the cache key. Defaults to `false`, which strips both from cacheable requests. :br :br
    Every client sending the same credential value shares one entry. When a response must never be shared, use `shouldBypassCache` instead.
  ::
  ::field{name="sendCacheControl" type="boolean"}
    Whether to automatically set a `cache-control` response header. Defaults to `true`. :br
    Set to `false` for server-only caching: responses are still stored and served from cache, but no `cache-control` header is advertised to clients and CDNs. This does not emit `no-store`, so downstream caches may still store the response under their own heuristics.
  ::
  ::field{name="cacheStatusHeader" type="boolean | string"}
    Add a cache status response header (`X-Cache: HIT | STALE | REVALIDATED | MISS`). Defaults to `true`. Pass a string to use a custom header name, or `false` to disable.
  ::
  ::field{name="stream" type="boolean"}
    Stream the response that fills a cache entry instead of buffering it first. Defaults to `false`. :br
    Improves time to first byte for streaming renders, at the cost of a synthesized `etag` (the digest needs a finished body) and mid-body error recovery. Nothing partial is ever stored, and later requests are served the stored entry as usual.
  ::
  ::field{name="maxBodySize" type="number"}
    Largest response body, in bytes, that may be buffered for storage. A larger response streams through uncached, exactly as a bypassed request does. :br
    Defaults to the per-entry ceiling of the [cache storage](#cache-storage): derived from `cache.memory.maxBytes` for the `memory` driver (about 50 MB with the default 100 MB, since binary bodies are budgeted at twice their size). The `fs` and `kv` drivers declare no ceiling, so **there is no default limit** with them. Set this on any route that proxies upstream responses whose size you do not control.
  ::
  ::field{name="shouldCache()" type="(entry: ResponseCacheEntry) => boolean | Promise<boolean>"}
    Additional predicate deciding whether a response is cacheable, running on top of the [built-in checks](#what-gets-cached) (which always apply and can only be narrowed). Return `false` to skip caching a response; it is still returned to the caller, just not stored. :br
    It runs on reads as well as writes, may be async, and fails closed: a throwing hook counts as "not cacheable" and reaches `onError`.
  ::
::

### Function-only options

These options are only available for `defineCachedFunction`:

::field-group
  ::field{name="transform()" type="(entry: CacheEntry, ...args) => any"}
    Transform the cache entry before returning. The return value replaces the cached value. :br
    The entry also carries a per-call `entry.status` (`"hit"`, `"stale"`, `"revalidated"` or `"miss"`) describing how the value was served, useful for metrics or conditional logic.
  ::
  ::field{name="serialize()" type="(entry: CacheEntry, ctx: { args }) => any"}
    Prepare the resolved value for storage, right before the entry is persisted (the write-side counterpart of `transform`). Useful when the function returns something that cannot be stored as-is (e.g. a stream or a class instance): `serialize` converts it on write and `transform` restores it on read. :br
    It runs once per resolution and is shared by deduplicated callers, so it may safely consume a one-use source such as a stream.
  ::
  ::field{name="validate()" type="(entry: CacheEntry, ctx: { args }) => boolean | Promise<boolean>"}
    Validate a cache entry. Return `false` (or a Promise resolving to `false`) to treat the entry as invalid and trigger re-resolution. The second argument carries the `args` of the current call, so the entry can be validated against it.
  ::
::

## SWR behavior

The `stale-while-revalidate` (SWR) pattern is **opt-in** via the `swr` option (disabled by default). Understanding how it interacts with other options:

| `swr` | `maxAge` | Behavior |
|-------|----------|----------|
| `false` (default) | `3600` | Cache for 1 hour, wait for the fresh value when expired |
| `true` | `3600` | Cache for 1 hour, serve stale while revalidating |
| `true` | `3600` with `staleMaxAge: 600` | Cache for 1 hour, serve stale for up to 10 more minutes while revalidating |
| `true` | `3600` with `staleMaxAge: 0` | Cache for 1 hour, never serve stale (same as `swr: false`) |

When `swr` is enabled and a cached value exists but has expired:
1. The stale cached value is returned immediately to the client.
2. The function/handler is called in the background to refresh the cache.
3. On edge workers, `event.waitUntil` is used to keep the background refresh alive.

When `swr` is disabled (default) and a cached value has expired:
1. The client waits while the function/handler resolves a fresh value.
2. The entry is replaced before the call returns (a `REVALIDATED` status).

::tip
Enable `swr` when slightly outdated data is acceptable (content pages, listings, mirrored third-party APIs) so response times stay flat even when the cache expires. Keep it disabled when callers must never receive stale data, such as access tokens or per-request authorization checks.
::

::warning
With `swr: true` and **no** `staleMaxAge`, entries carry no storage TTL: the last good value is served until the backend evicts it. That is exactly the ISR pattern, but it means cache growth is bounded by your backend's capacity, not by time. Set `staleMaxAge` for eventual cleanup.
::

## Cache keys

When using the `defineCachedFunction` or `defineCachedHandler` functions, the cache key is generated using the following pattern:

```ts
`${options.base}:${options.group}:${options.name}:${options.getKey(...args)}.json`
```

`group` and `name` are **escaped** before they enter the key: characters outside `[A-Za-z0-9_]` are removed and, when that changed the value, a hash of the raw value is appended. That is why Nitro's `nitro/functions` group appears as `nitrofunctions.<hash>` and cannot be confused with the `:` key structure. The `getKey` result is the terminal segment and is stored exactly as returned.

For example, the following function:

```ts
import { defineCachedFunction } from "nitro/cache";

const getAccessToken = defineCachedFunction(() => {
  return String(Date.now())
}, {
  maxAge: 10,
  name: "getAccessToken",
  getKey: () => "default"
});
```

Will be stored under a key like:

```ts
cache:nitrofunctions.3aFYDY_G88ZLdElv-hEB0r8snSViMYJ1NbRtlNP9o_0:getAccessToken:default.json
```

The default `base` is `/cache`, normalized to the `cache:` prefix (as with any other `/` in the key).

::note
For cached handlers, the generated key covers the request **origin** (scheme, host and port), the URL path, and the method, plus hashes of the values behind [`varies`](#handler-only-options), [`allowQuery`](#handler-only-options) and [`allowCookies`](#handler-only-options). `GET` and `HEAD` are stored separately.
::

::important
The `name` falls back to a hash of the **function source**, which cannot tell apart handlers or functions produced by a factory or a loop: they have identical source but different closed-over variables, so they share a key and overwrite each other's entries. Always pass an explicit `name` (or `getKey`) for those.
::

::note
Without `getKey`, the arguments are hashed. Opaque values (`Blob`, `ReadableStream`, `Promise`, `Request`, `WeakMap`) contribute only their type, so two different ones share an entry, and arguments nested deeper than 128 levels throw a `RangeError`. Pass a `getKey` reading only the fields the result depends on when either applies.
::

### What gets cached

Before a response is stored, the following checks apply. A response that fails any of them is still returned to the caller, it is just never stored or served from the cache:

| Check | Reason |
| ----- | ------ |
| Status is not `200`, `203`, `301` or `308` | Only these are complete, reusable representations. Errors, `204`/`304`, `206` ranges and temporary redirects are excluded. |
| `cache-control` opt-out from the handler | `no-store`, `private`, `no-cache`, or a zero shared lifetime (`s-maxage` when present, else `max-age`). |
| `Vary: *` | The strongest "do not share" signal short of `no-store`. |
| A `Vary` naming a header outside the cache key | Storing it would replay one variant to every client. Add the header to [`varies`](#handler-only-options) instead. |
| A missing body | Nothing to replay. An empty (zero-byte) `200` is fine and caches normally. |
| An `etag` or `last-modified` equal to the literal string `"undefined"` | The usual result of stringifying a missing value; it would break conditional requests. |

These checks always apply. Use [`shouldCache`](#handler-only-options) to narrow further. A response rejected by a built-in check also gets **no** synthesized `cache-control`, so a CDN in front cannot hold on to an error; a response rejected by your `shouldCache` still does.

::note
`cache-control: must-revalidate` is not an opt-out. The response is stored and served while fresh, but never while stale, so SWR is disabled for that response only.
::

::read-more{to="https://ocache.unjs.io/docs/cache-control"}
ocache: cache-control and eligibility in full.
::

## Cache invalidation

Cached entries can be invalidated programmatically at runtime (for example from a webhook when the underlying data changes) without waiting for `maxAge` to expire.

### `.invalidate()` and `.expire()` methods

Every function created with `defineCachedFunction` exposes on-demand revalidation methods:

- **`.invalidate(...args)`**: removes the cached entry entirely. The next call re-invokes the function and waits for the fresh value.
- **`.expire(...args)`**: marks the cached entry as stale without removing it. With `swr` enabled, the stale value is still served while the next call triggers a background refresh; without SWR, the next call re-resolves before returning.
- **`.resolveKeys(...args)`**: resolves the storage key(s) the entry is cached under (one per `base` prefix).

Arguments are passed through `getKey` to generate the cache key.

```ts
import { defineCachedFunction } from "nitro/cache";

const cachedGHStars = defineCachedFunction(async (repo: string) => {
  const data = await fetch(`https://api.github.com/repos/${repo}`).then(res => res.json());
  return data.stargazers_count;
}, {
  maxAge: 60 * 60,
  name: "ghStars",
  getKey: (repo: string) => repo,
});

await cachedGHStars("unjs/nitro"); // populates the cache
await cachedGHStars.expire("unjs/nitro"); // marks the entry as stale
await cachedGHStars.invalidate("unjs/nitro"); // removes the entry
await cachedGHStars("unjs/nitro"); // re-invokes the function
```

If no cached entry matches the given arguments, `.invalidate()` and `.expire()` resolve without error and leave storage unchanged. Both also cancel a resolution already in flight for that key, so a pre-purge value cannot be written after the purge.

::note
Nitro's `defineCachedHandler` currently returns a plain event handler and does not expose these methods, so a cached route cannot be invalidated on demand. When you need that, move the expensive work into a `defineCachedFunction` called from a plain `defineHandler` and invalidate the function instead.
::

## Cache storage

Cache storage is configured with the [`cache`](/config#cache) option. The `driver` option selects where entries are stored:

| Driver | Storage | Persisted |
| ------ | ------- | --------- |
| `memory` (default) | In-process LRU memory storage (ocache's `createMemoryStorage`). | No |
| `fs` | One file per entry under `cache.fs.dir` (defaults to `.data/cache`). Node.js compatible runtimes only. | Yes |
| `kv` | The [KV storage layer](/docs/storage) (`useKV()`), under the `cache:` prefix. Installs `unstorage` on demand. | Depends on the mount |

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

export default defineConfig({
  cache: {
    driver: "fs",
    fs: { dir: ".data/cache" },
  },
});
```

The `memory` driver keeps entries in the server process, so they are **not persisted across restarts** or shared between workers. It evicts least recently used entries once it holds `cache.memory.maxSize` entries (default `10000`) or `cache.memory.maxBytes` bytes (default 100 MB):

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

export default defineConfig({
  cache: {
    memory: { maxSize: 1000, maxBytes: 32 * 1024 * 1024 },
  },
});
```

The `fs` driver never deletes expired files on its own (expired entries are not served and get overwritten on the next write). Responses varying by request values (`varies`, `allowQuery`) can create many files, so clean up the directory periodically.

With the `fs` driver, a relative `dir` resolves against `rootDir` in development and while prerendering, and against the working directory of the server process in production.

Use `$development` (and `$prerender`) to use a different driver during development:

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

export default defineConfig({
  cache: { driver: "kv" },
  $development: {
    cache: { driver: "fs" },
  },
});
```

### KV storage

The `kv` driver stores entries in the [KV storage layer](/docs/storage), which lets you use any [unstorage driver](https://unstorage.unjs.io/drivers) (Redis, Cloudflare KV, etc.) as cache backend. Mount it at the `cache` mount point using the `kv` option:

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

export default defineConfig({
  kv: {
    cache: {
      driver: "redis",
      /* redis connector options */
    },
  },
});
```

When a `cache` mount point (or one under the `cache:` / `cache/` prefix) is configured, `cache.driver` defaults to `kv`, so existing configurations keep storing cache entries in it. Set `cache.driver` explicitly to opt out. Mounts added at runtime (for example `useKV().mount("cache", ...)` in a plugin) cannot be detected at build time, so set `cache: { driver: "kv" }` explicitly in that case.

::important
If your cached functions, handlers or route rules use a custom [`base`](#shared-options) to store entries in another KV mount point (for example `base: "redis"`), set `cache: { driver: "kv" }` explicitly. Otherwise entries are stored with the default `memory` driver.
::

::note
Entries are serialized as JSON through unstorage, so the `kv` driver does not declare `binary`. A response body that decodes as UTF-8 (HTML, JSON, plain text) is stored as text either way; only a body that is **not** valid UTF-8 (images, fonts, PDFs) is base64-encoded, as are byte-valued cached functions. The `memory` and `fs` drivers declare `binary` and keep those bytes as bytes. :br :br
The `kv` driver also declares no per-entry size ceiling, which means cached handlers buffer any response body unless you set [`maxBodySize`](#handler-only-options).
::

### Global options

Options in `cache.defaults` are used as defaults for every cached function, cached handler and `cache` route rule. Options passed to `defineCachedFunction`, `defineCachedHandler` or a route rule take precedence.

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

export default defineConfig({
  cache: {
    defaults: {
      maxAge: 60,
      swr: true,
      staleMaxAge: 600,
      cacheStatusHeader: false,
    },
  },
});
```

Shared defaults are `maxAge`, `swr`, `staleMaxAge`, `maxResolveTime` and `base`. Handler-only defaults (`varies`, `allowQuery`, `allowCookies`, `allowAuthorization`, `sendCacheControl`, `cacheStatusHeader` and `maxBodySize`) only apply to cached handlers and route rules.

::read-more{to="https://ocache.unjs.io/docs/storage"}
ocache: the storage interface, binary payloads and layered backends.
::

::read-more{to="/docs/storage"}
Read more about the Nitro storage layer and available drivers.
::

## Using route rules

Route rules let you enable caching for all routes matching a glob pattern, directly from your configuration. This is especially useful for a global cache strategy across a part of your application, without touching handler code.

Cache all the blog routes for 1 hour:

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

export default defineConfig({
  routeRules: {
    "/blog/**": { cache: { maxAge: 60 * 60 } },
  },
});
```

The `cache` rule accepts the same [handler options](#options) documented above (`swr`, `staleMaxAge`, `varies`, `allowQuery`, `allowCookies`, `allowAuthorization`, `sendCacheControl`, `cacheStatusHeader`, `stream`, `maxBodySize`, `headersOnly`, `base`, `group`, `name`, `integrity`, `maxResolveTime`). Hook options such as `shouldCache` are only available on `defineCachedHandler`.

If we want to use a custom [KV storage](#kv-storage) mount point, we can use the `base` option with the `kv` cache driver.

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

export default defineConfig({
  cache: { driver: "kv" },
  kv: {
    redis: {
      driver: "redis",
      url: "redis://localhost:6379",
    },
  },
  routeRules: {
    "/blog/**": { cache: { maxAge: 60 * 60, base: "redis" } },
  },
});
```

### Route rules shortcuts

You can use the `swr` shortcut for enabling `stale-while-revalidate` caching on route rules. `swr: 3600` is shorthand for `cache: { swr: true, maxAge: 3600 }`. When set to `true`, SWR is enabled with the default `maxAge` of **1 second**, so prefer passing an explicit number.

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

export default defineConfig({
  routeRules: {
    "/blog/**": { swr: 3600 },
    "/api/**": { swr: 60 },
  },
});
```

To explicitly disable caching on a route, set `cache: false` (or `swr: false`) on a more specific pattern. This also removes a `cache` rule inherited from a broader pattern:

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

export default defineConfig({
  routeRules: {
    "/api/**": { swr: 60 },
    "/api/realtime/**": { cache: false },
  },
});
```

::note
When using route rules, cached handlers use the group `'nitro/route-rules'` instead of the default `'nitro/handlers'`, and the generated `name` scopes the entry to the matched route handler, the HTTP method, the rule pattern and the matched route.
::

::warning
That generated scope is unique per process. With a **persistent** cache storage, a restart or a second worker therefore starts from fresh entries rather than reusing the ones already stored. Set an explicit `name` on the rule when route-rule cache entries need to be shared across processes.
::

::read-more{to="/docs/routing#route-rules"}
Route rules can do much more than caching: headers, redirects, proxying, and more.
::

---

### 1.Docs 8.Storage (`docs/1.docs/8.storage.md`)

---
icon: carbon:datastore
---

# KV Storage

> Nitro provides a built-in storage layer that can abstract the filesystem, a database, or any other data source.

Nitro has built-in integration with [unstorage](https://unstorage.unjs.io) to provide a runtime-agnostic persistence layer: your code always calls the same key-value API, and the backing driver (memory, filesystem, Redis, ...) is swapped through configuration.

::note
`unstorage` is resolved from your project and installed automatically (without prompting): once you import `nitro/kv` (or `nitro/storage`), configure [`kv`](/config#kv) mounts, or use the `kv` [cache driver](/docs/cache#cache-storage).
::

## Usage

To use the storage layer, you can use the `useKV()` utility to access the storage instance.

```ts
import { useKV } from "nitro/kv";

// Default storage (in-memory)
await useKV().setItem("test:foo", { hello: "world" });
const value = await useKV().getItem("test:foo");

// You can specify a base prefix with useKV(base)
const testStorage = useKV("test");
await testStorage.setItem("foo", { hello: "world" });
await testStorage.getItem("foo"); // { hello: "world" }

// You can use generics to type the return value
await useKV<{ hello: string }>("test").getItem("foo");
await useKV("test").getItem<{ hello: string }>("foo");
```

:read-more{to="https://unstorage.unjs.io"}

::note
`useKV` was previously named `useStorage` and exported from `nitro/storage`, and the `kv` config option was previously named `storage`. The old names still work as deprecated aliases. The deprecated `devStorage` option is replaced by `kv` inside `$development` (and `$prerender`).
::

### Available methods

The storage instance returned by `useKV()` provides the following methods:

| Method | Description |
|---|---|
| `getItem(key)` | Get the value of a key. Returns `null` if the key does not exist. |
| `getItems(items)` | Get multiple items at once. Accepts an array of keys or `{ key, options }` objects. |
| `getItemRaw(key)` | Get the raw value of a key without parsing. Useful for binary data. |
| `setItem(key, value)` | Set the value of a key. |
| `setItems(items)` | Set multiple items at once. Accepts an array of `{ key, value }` objects. |
| `setItemRaw(key, value)` | Set the raw value of a key without serialization. |
| `hasItem(key)` | Check if a key exists. Returns a boolean. |
| `removeItem(key)` | Remove a key from storage. |
| `getKeys(base?)` | Get all keys, optionally filtered by a base prefix. |
| `clear(base?)` | Clear all keys, optionally filtered by a base prefix. |
| `getMeta(key)` | Get metadata for a key (e.g., `mtime`, `atime`, `ttl`). |
| `setMeta(key, meta)` | Set metadata for a key. |
| `removeMeta(key)` | Remove metadata for a key. |
| `mount(base, driver)` | Dynamically mount a storage driver at a base path. |
| `unmount(base)` | Unmount a storage driver from a base path. |
| `watch(callback)` | Watch for changes. Callback receives `(event, key)` where event is `"update"` or `"remove"`. |
| `unwatch()` | Stop watching for changes. |

Shorthand aliases are also available: `get`, `set`, `has`, `del`, `remove`, `keys`.

```ts
import { useKV } from "nitro/kv";

// Get all keys under a prefix
const keys = await useKV("test").getKeys();

// Check if a key exists
const exists = await useKV().hasItem("test:foo");

// Remove a key
await useKV().removeItem("test:foo");

// Get raw binary data
const raw = await useKV().getItemRaw("assets:server:image.png");

// Get metadata (type, etag, mtime, etc.)
const meta = await useKV("assets:server").getMeta("file.txt");
```

## Configuration

You can mount one or multiple custom storage drivers using the `kv` option.

The key is the mount point name, and the value is the driver name and configuration.

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

export default defineConfig({
  kv: {
    redis: {
      driver: "redis",
      /* redis connector options */
    }
  }
})
```

Then, you can use the redis storage using the `useKV("redis")` function.

::read-more{to="https://unstorage.unjs.io"}
You can find the driver list on [unstorage documentation](https://unstorage.unjs.io/) with their configuration.
::

::important
Mount options are serialized into the build output, so they must be JSON-serializable. Use [runtime configuration](#runtime-configuration) if a mount needs a non-serializable option.
::

### Driver dependencies

Some drivers rely on a third-party library (for example, `redis` requires [`ioredis`](https://www.npmjs.com/package/ioredis)).

Nitro detects the libraries required by the mounted drivers and prompts to install the missing ones (installed automatically in CI). Installed libraries are then explicitly passed to the driver via its `lib` option, so that bundlers can statically resolve them. `lib` options are therefore not configurable in `kv` mounts (set them to `null` to opt out of the injected import).

### Development storage

You can use the `$development` config key to override `kv` mounts during development.

This is useful when your production driver is not available in development (e.g., a managed Redis instance).

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

export default defineConfig({
  kv: {
    db: {
      driver: "redis",
      host: "prod.example.com",
    }
  },
  $development: {
    kv: {
      db: {
        driver: "fs",
        base: "./.data/db"
      }
    }
  }
})
```

When running in development mode, `$development.kv` mounts are merged on top of `kv` mounts, allowing you to use a local filesystem driver or an in-memory driver while developing. A mount with a different `driver` replaces the whole mount (`host` is not passed to the `fs` driver above). A mount with the same `driver` has its options deep merged.

`$development` only applies to the development server. When [prerendering](/docs/prerendering) a production build, `$production` and then `$prerender` overrides apply instead. Use `$prerender` to override mounts during prerendering only:

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

export default defineConfig({
  kv: {
    db: { driver: "redis", host: "prod.example.com" }
  },
  $development: {
    kv: { db: { driver: "fs", base: "./.data/db" } }
  },
  $prerender: {
    kv: { db: { driver: "fs", base: "./.data/db" } }
  }
})
```

::tip
A common setup is a hosted driver (e.g. `redis`) in `kv` for production and an `fs` driver in `$development.kv`, so development data is easy to inspect locally in `.data/` and does not require external services.
::

## Built-in mount points

Nitro automatically mounts the following storage paths:

### `/assets`

Server assets are mounted at the `/assets` base path (see [Server assets](#server-assets)). In production, this mount is read-only and only supports `getKeys()`, `hasItem()`, `getItem()`, `getItemRaw()`, and `getMeta()`.

```ts
import { useKV } from "nitro/kv";

// Access server assets via the /assets mount
const content = await useKV("assets:server").getItem("my-file.txt");
```

### Default (in-memory)

The root storage (without a base path) uses an in-memory driver by default. Data stored here is not persisted across restarts.

```ts
import { useKV } from "nitro/kv";

// In-memory by default, not persisted
await useKV().setItem("counter", 1);
```

To persist data, mount a driver with a persistent backend (e.g., `fs`, `redis`, etc.) using the `kv` configuration option.

::note
The [cache layer](/docs/cache) uses its own in-memory storage by default. Configuring a `cache` mount point (or setting [`cache.driver`](/config#cache) to `kv`) stores cache entries in this storage layer instead.
::

## Server assets

Nitro allows you to bundle files from an `assets/` directory. These files are accessible at runtime via the `assets:server` storage mount.

The directory is resolved relative to [`serverDir`](/config#serverdir) when it is set, and to the project root otherwise:

```
my-project/
  server/
    assets/
      data.json
      templates/
        welcome.html
    routes/
      index.ts
```

```ts [server/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 template = await serverAssets.getItem("templates/welcome.html");

  return { keys, data, template };
});
```

### Custom asset directories

You can register additional asset directories using the `serverAssets` config option. Each `dir` is resolved relative to `rootDir` (not `serverDir`):

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

export default defineConfig({
  serverAssets: [
    {
      baseName: "templates",
      dir: "./templates",
    }
  ]
})
```

Custom asset directories are accessible under `assets:<baseName>`:

```ts
import { useKV } from "nitro/kv";

const templates = useKV("assets:templates");
const keys = await templates.getKeys();
const html = await templates.getItem("email.html");
```

### Asset metadata

Server assets include metadata such as content type, ETag, and modification time:

```ts
import { useKV } from "nitro/kv";

const serverAssets = useKV("assets:server");

const meta = await serverAssets.getMeta("image.png");
// { type: "image/png", etag: "\"...\"", mtime: "2024-01-01T00:00:00.000Z" }

// Useful for setting response headers
const raw = await serverAssets.getItemRaw("image.png");
```

::note
In development (and while prerendering), server assets are read directly from the filesystem with the `fs` driver, so `getMeta()` returns filesystem stats (`mtime`, `size`, ...) instead of `type` and `etag`. In production, assets are bundled into the build output with their metadata pre-computed at build time.
::

::read-more{to="/docs/assets"}
Read more about public and server assets.
::

## Runtime configuration

In scenarios where the mount point configuration is not known until runtime, Nitro can dynamically add mount points during startup using [plugins](/docs/plugins).

```ts [plugins/storage.ts]
import { useKV } from "nitro/kv";
import { definePlugin } from "nitro";
import redisDriver from "unstorage/drivers/redis";

export default definePlugin(() => {
  const storage = useKV()

  // Dynamically pass in credentials from runtime configuration, or other sources
  const driver = redisDriver({
    base: "redis",
    host: process.env.REDIS_HOST,
    port: Number(process.env.REDIS_PORT),
    // Pass the driver library explicitly so that it is bundled
    lib: () => import("ioredis"),
    /* other redis connector options */
  })

  // Mount driver
  storage.mount("redis", driver)
})
```

::important
Drivers mounted this way are outside of Nitro's [driver dependency detection](#driver-dependencies): install the third-party library yourself (`ioredis` in the example above) and pass it via the driver's `lib` option. Without it, the driver falls back to a dynamic `import("ioredis")` that bundlers cannot statically resolve, and the mount fails at runtime with a `Cannot import ioredis` error.
::

---

### 1.Docs 99.Migration (`docs/1.docs/99.migration.md`)

---
icon: ri:arrow-right-up-line
---

# Migration Guide

> Nitro v3 introduces intentional backward-incompatible changes. This guide helps you migrate from Nitro v2.

> [!NOTE]
> This is a living document for migrating from Nitro 2 to 3. Please check it regularly while using the beta version.

Most migrations follow the same order:

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

## `nitropack` Is Renamed to `nitro`

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

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

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

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

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

```diff
-- import { defineNitroConfig } from "nitropack/config"
++ import { defineConfig } from "nitro"
```

## Minimum Supported Node.js Version: 22

Nitro now requires a minimum Node.js version of 22.22.0, as Node.js 20 reached end-of-life in [April 2026](https://nodejs.org/en/about/previous-releases).

Please upgrade to the [latest LTS](https://nodejs.org/en/download) version (>= 22.22.0).

**Migration:**

- Check your local Node.js version using `node --version` and update if necessary.
- If you use a CI/CD system for deployment, ensure that your pipeline is running Node.js 22.22.0 or higher.
- If your hosting provider manages the Node.js runtime, make sure it's set to version 22, 24, or later.

## Builder Is Installed On Demand

Nitro bundles your server with `rolldown` by default (or [`rollup` or `vite`](/config#builder)), but none of them is a dependency of Nitro. The builder is resolved from your project and installed on demand when building. Add it to your project dependencies to pin its version:

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

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

## Auto-Imports Are Removed

Nitro v2 automatically made utilities such as `defineEventHandler`, `useStorage`, `useRuntimeConfig`, `defineCachedFunction`, and `defineNitroPlugin` available in server code without imports (powered by [unimport](https://github.com/unjs/unimport)), and also exposed the exports of `server/utils/` and a virtual `#imports` module.

Auto-imports have been **removed** in Nitro v3. The `imports` config option, the `#imports` module, and the generated `nitro-imports.d.ts` declarations are no longer available. Using these globals will result in build or runtime errors.

**Migration:** Add explicit imports everywhere. Common utilities and where they come from:

```ts
import { defineHandler, definePlugin, defineErrorHandler, HTTPError } from "nitro";
import { useKV } from "nitro/kv";
import { useRuntimeConfig } from "nitro/runtime-config";
import { defineCachedFunction, defineCachedHandler } from "nitro/cache";
import { useDatabase } from "nitro/database";
import { defineTask, runTask } from "nitro/task";
import { useNitroApp, useNitroHooks } from "nitro/app";
import { getQuery, getCookie } from "nitro/h3";
```

Utilities from your own `utils/` directory need explicit (relative) imports too:

```ts [server/routes/index.ts]
import { defineHandler } from "nitro";
import { makeGreeting } from "../utils/hello.ts";

export default defineHandler(() => makeGreeting("Nitro"));
```

## Type Imports

Nitro types are now only exported from `nitro/types`.

**Migration:** Import types from `nitro/types` instead of `nitro`:

```diff
-- import { NitroRuntimeConfig } from "nitropack"
++ import { NitroRuntimeConfig } from "nitro/types"
```

## Changed Nitro Subpath Imports

Nitro v2 introduced multiple subpath exports, some of which have been removed or updated:

- `nitropack/rollup`, `nitropack/core` (use `nitro/builder`)
- `nitropack/runtime/*` (use `nitro/*`)
- `nitropack/kit` (removed)
- `nitropack/presets` (removed)
- `nitropack/meta` (use `nitro/meta`, which only exports `version`, `runtimeDir` and `runtimeDependencies`)

An experimental `nitropack/kit` was introduced but has now been removed. A standalone Nitro Kit package may be introduced in the future with clearer objectives.

**Migration:**

- Use `NitroModule` from `nitro/types` instead of `defineNitroModule` from the kit.
- Prefer built-in Nitro presets (external presets are only for evaluation purposes).

## Server Directory Scanning Is Opt-In

In Nitro v2, `srcDir` defaulted to the project root, and directories such as `routes/`, `api/`, `middleware/`, `plugins/`, and `tasks/` were always scanned.

In Nitro v3:

- The `srcDir` option is deprecated in favor of `serverDir` (using `srcDir` still works but logs a warning).
- `serverDir` defaults to `false`, meaning **no directory scanning happens** until you explicitly enable it. Scanned routes, middleware, plugins, and tasks won't be registered otherwise.

**Migration:** Set `serverDir` in your Nitro config:

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

export default defineConfig({
  serverDir: "./server", // or "." to keep the v2 root layout
});
```

Setting `serverDir: true` is a shortcut for `"server"`. Paths are resolved relative to the project root.

## Reserved Suffixes in Scanned Files

Nitro v3 reserves trailing lowercase suffixes in scanned file names. In v2, `routes/hello.world.ts` was registered as `/hello.world`. In v3, the build fails with an unknown suffix error (in dev, the file is skipped with an error log). Known file extensions such as `routes/sitemap.xml.ts` keep working.

`.dev`, `.prod` and `.prerender` in `middleware/`, `plugins/`, `tasks/` and `modules/` file names are reserved too, so they also fail the build.

Test files (`*.test.ts`, `*.spec.ts`) and type declarations (`*.d.ts`) are no longer registered.

**Migration:** Rename the file, or add the suffix to [`scanSuffixes`](/config#scansuffixes) to keep it as part of the name.

## `unenv` Option Is Deprecated

The `unenv` config option (and `nitro.options.unenv` in hooks) is deprecated in favor of the [`alias`](/config#alias), [`inject`](/config#inject), [`polyfills`](/config#polyfills) and [`builtinModules`](/config#builtinmodules) options (unenv's `polyfill` and `external` map to `polyfills` and `builtinModules`). Presets passed to `unenv` still work, and a warning is logged.

```diff
export default defineConfig({
-  unenv: {
-    alias: { "my-module": "my-module/web" },
-    external: ["cloudflare:workers"],
-  },
+  alias: { "my-module": "my-module/web" },
+  builtinModules: ["cloudflare:workers"],
});
```

Modules that pushed presets from hooks should update these options directly (e.g. `nitro.options.builtinModules.push(...)`). Values set in these options override the defaults added by deployment presets (e.g. Cloudflare and Deno map supported Node.js builtins to native modules).

`unenv` is no longer a dependency of Nitro. When a [`node: false`](/config#node) build imports a Node.js built-in module that the target runtime does not support, `unenv` is resolved from your project and installed automatically (without prompting). Add it to your project dependencies if you import `unenv` directly.

## `unstorage` and `db0` Are Installed On Demand

`unstorage` and `db0` are no longer dependencies of Nitro. They are resolved from your project and installed on demand when used:

- `unstorage`: when you import `nitro/kv` (or `nitro/storage`), configure [`kv`](/config#kv) mounts, or set [`cache.driver`](/config#cache) to `"kv"`.
- `db0`: when [`experimental.database`](/docs/database) is enabled or you import `nitro/database`.

They are installed into your project dependencies without prompting. Add them yourself if you import `unstorage` or `db0` directly.

## App Config Support Removed

Nitro v2 supported a bundled app config that allowed defining configurations in `app.config.ts` and accessing them at runtime via `useAppConfig()`.

This feature has been removed.

**Migration:**

Use a regular `.ts` file in your server directory and import it directly.

## Route Rules

Route rule matching and handling moved to [h3-rules](https://github.com/h3js/h3-rules). The rule shapes for `redirect`, `proxy`, `cors`, `headers`, `cache`, and `swr` are now defined by h3-rules, while `isr`, `prerender`, and `static` remain Nitro-specific rules.

The `NitroRouteConfig` and `NitroRouteRules` types are deprecated in favor of `RouteRuleConfig` and `NormalizedRouteRules`, re-exported from `nitro/types`:

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

`getRouteRules(event)` is replaced by `event.context.routeRules`. To resolve the rules of another path, use `getRouteRules(method, pathname).routeRules` from `nitro/app`.

```diff
- import { getRouteRules } from "nitropack/runtime"
- const rules = getRouteRules(event)
+ import { getRouteRules } from "nitro/app"
+ const rules = event.context.routeRules
+ const { routeRules: blogRules } = getRouteRules("GET", "/blog/post")
```

## Runtime Utilities

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

```diff
-- import { useStorage } from "nitropack/runtime/storage"
++ import { useKV } from "nitro/kv"
```

| Utilities                                         | Nitro v3 Import        |
|---------------------------------------------------|------------------------|
| `useKV` (formerly `useStorage`)                   | `nitro/kv`             |
| `defineCachedFunction`, `defineCachedHandler`     | `nitro/cache`          |
| `useDatabase`                                     | `nitro/database`       |
| `useRuntimeConfig`                                | `nitro/runtime-config` |
| `defineTask`, `runTask`                           | `nitro/task`           |
| `useNitroApp`, `useNitroHooks`, `getRouteRules`   | `nitro/app`            |

::note
`useStorage` has been renamed to `useKV` and `nitro/storage` to `nitro/kv`. The `storage` config option has been renamed to `kv`, and `devStorage` is replaced by `kv` inside `$development` (add it to `$prerender` too if you relied on `devStorage` while prerendering). The old names still work as deprecated aliases and will be removed in a future version.
::

## Cache Storage

Cached functions and handlers no longer use the storage layer by default. Entries are stored in an in-memory LRU storage, configurable with the new [`cache`](/config#cache) option (`driver: "memory" | "fs" | "kv"`).

If you configured a `cache` storage mount point (for example `storage: { cache: { driver: "redis" } }`), cache entries keep being stored in it. If your cached functions, handlers or route rules use a custom `base` pointing to another mount point, or you mount the cache storage at runtime (`useKV().mount(...)` in a plugin), set `cache: { driver: "kv" }`.

## Internal Server Fetch

To make internal requests to your own server (the v2 `useNitroApp().localFetch` pattern), use the `serverFetch` utility exported from `nitro`. There is also a `fetch` export that routes absolute paths (starting with `/`) to the server and everything else to global `fetch`.

```ts
import { serverFetch } from "nitro";

const res = await serverFetch("/api/hello"); // returns a web Response
```

## Plugins

The `defineNitroPlugin` utility has been renamed to `definePlugin` and is now imported from `nitro`.

```diff
-- export default defineNitroPlugin((nitroApp) => {
++ import { definePlugin } from "nitro"
++
++ export default definePlugin((nitroApp) => {
```

## Optional Hooks

If you were using `useNitroApp().hooks` outside of Nitro plugins before, it might be undefined. Use new `useNitroHooks()` to guarantee having an instance.

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

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

## Build Hooks Renamed

The `rollup:before` and `rollup:reload` build hooks run for all builders (Rollup, Rolldown and Vite), so they are renamed to `build:config` and `build:reload`. The `build:config` hook receives `{ builder, rollup }` or `{ builder, rolldown }` instead of the bare bundler config. With the Vite builder, `builder` is `"vite"` and the config is the one for the bundler Vite uses.

```diff
-- nitro.hooks.hook("rollup:before", (nitro, config) => {
--   config.plugins.push(myPlugin());
++ nitro.hooks.hook("build:config", (nitro, { builder, rollup, rolldown }) => {
++   const config = rollup ?? rolldown;
++   config.plugins.push(builder === "rollup" ? myRollupPlugin() : myPlugin());
   });
```

The old names still work as deprecated aliases (a warning is logged): `rollup:before` hooks are called from `build:config` with the bare config, and `rollup:reload` hooks are called by `build:reload`. Calling the old names with `nitro.hooks.callHook()` no longer runs any hooks.

## Error Handler

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

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

## H3 v2

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

### Web Standards

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

Access to `event.node.{req,res}` is only available in Node.js runtime. `event.web` is renamed to `event.req` (instance of web [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request)).

### Response Handling

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

```diff
-- import { send, sendRedirect, sendStream } from "nitro/h3"
-- send(event, value)
-- sendStream(event, stream)
-- sendRedirect(event, location, code)
++ import { redirect } from "nitro/h3"
++ return value
++ return stream
++ return redirect(location, code)
```

Other changes:

- `sendError(event, error)` → `throw error` (or `throw new HTTPError(...)`)
- `sendNoContent(event)` → `return noContent()`
- `sendProxy(event, target)` → `return proxy(event, target)`

### Request Body

Most body utilities can be replaced with native `event.req` methods:

```diff
-- import { readBody, readRawBody, readFormData } from "nitro/h3"
++ // Use native Request methods
++ const json = await event.req.json()
++ const text = await event.req.text()
++ const formData = await event.req.formData()
++ const stream = event.req.body
```

### Headers

H3 now uses standard web [`Headers`](https://developer.mozilla.org/en-US/docs/Web/API/Headers). Header values are always plain `string` (no `null`, `undefined`, or `string[]`).

```diff
-- import { getHeader, setHeader, getResponseStatus } from "nitro/h3"
-- getHeader(event, "x-foo")
-- setHeader(event, "x-foo", "bar")
++ event.req.headers.get("x-foo")
++ event.res.headers.set("x-foo", "bar")
++ event.res.status // instead of getResponseStatus(event)
```

### Handler Utils

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

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

### Error Utils

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

### Node.js Utils

```diff
-- import { defineNodeListener, fromNodeMiddleware, toNodeListener } from "nitro/h3"
++ import { defineNodeHandler, fromNodeHandler, toNodeHandler } from "nitro/h3"
```

## Preset Updates

Nitro presets have been updated for the latest compatibility.

Some (legacy) presets have been removed or renamed.

| Old Preset                   | New Preset                    |
|------------------------------|-------------------------------|
| `node` (Node.js middleware)  | `node_middleware` (export changed to `middleware`) |
| `cloudflare`, `cloudflare_worker`, `cloudflare_module_legacy` | `cloudflare_module`           |
| `deno-server-legacy`         | `deno_server` with Deno v2    |
| `netlify-builder`            | `netlify` or `netlify_edge`   |
| `vercel-edge`                | `vercel` with Fluid compute enabled |
| `azure`, `azure_functions`   | `azure_swa`                   |
| `firebase`                   | `firebase_app_hosting`        |
| `iis`                        | `iis_handler`                 |
| `deno` (Deno Deploy)         | `deno_server`                 |
| `deno_deploy`                | `deno_server` (Deno Deploy Classic shut down) |
| `winterjs`                   | Removed (WinterJS was archived upstream) |
| `edgio`                      | Discontinued     |
| `cli`                        | Removed due to lack of use |
| `service_worker`             | Removed due to instability |

::warning
The `node` and `deno` preset names still exist in v3, but they now point to different targets:

- In v2, the `node` preset produced a reusable Node.js middleware. In v3, `node` is an alias of `node_server`, which builds a full standalone Node.js server. If you keep `preset: "node"`, you now get a standalone server. Use `node_middleware` to keep the v2 middleware output.
- In v2, the `deno` and `deno_deploy` presets targeted Deno Deploy Classic (bundled to `server/index.ts` and deployed with `deployctl`). Deno Deploy Classic shut down on July 20, 2026, and `deployctl` is archived. In v3, `deno` is an alias of `deno_server`, which builds `.output/server/index.mjs`. The new Deno Deploy runs this output (see the [Deno Deploy guide](/deploy/runtimes/deno#deno-deploy)), and Nitro auto-detects `deno_server` when building on Deno Deploy.
::

## AWS Amplify Default Runtime

The `aws_amplify` preset now defaults the compute runtime to `nodejs22.x` (previously `nodejs20.x`), as Node.js 20 reached end-of-life in April 2026.

**Migration:**

- Test your app on Node.js 22, or pin a runtime explicitly with `awsAmplify: { runtime: "nodejs24.x" }` in your Nitro config.

## Cloudflare Dev and Vercel Observability Ignore Compatibility Date

Two preset features that were gated on `compatibilityDate` are now always enabled, so configs carried over from v2 with older dates keep them:

- The `cloudflare_module` and `cloudflare_pages` presets always use the `cloudflare-dev` preset (Miniflare emulation with bindings) in development. Previously, a `compatibilityDate` before `2025-07-13` silently fell back to the generic `nitro-dev` preset.
- The `vercel` preset always generates observability routes. Previously, they required a `compatibilityDate` of `2025-07-15` or later.

## Azure Functions v4 Programming Model

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

:pm-install{name="@azure/functions@^4"}

**Migration:**

- Add `@azure/functions@^4` to your dependencies (Nitro offers to install it when missing). If you have v3 installed, update it.
- To keep the legacy v3 programming model, set `azure: { functionsVersion: 3 }` in your Nitro config.

## Vercel ISR Route Rules

In Nitro v2, the `vercel` presets translated `swr`, `static` and `cache: false` route rules into Vercel ISR config. In Nitro v3, only the [`isr` route rule](/deploy/providers/vercel#fine-grained-isr-config-via-route-rules) configures ISR on Vercel, and `swr`/`cache` rules use Nitro's built-in caching layer like on other platforms. The `future.nativeSWR` option, which opted in to this behavior, has been removed.

**Migration:**

Use the `isr` route rule where you relied on Vercel ISR, and remove `future.nativeSWR` from your config.

```diff [nitro.config.ts]
export default defineConfig({
--  future: { nativeSWR: true },
  routeRules: {
--    "/blog/**": { swr: true },
--    "/api/realtime/**": { cache: false },
++    "/blog/**": { isr: true },
++    "/api/realtime/**": { isr: false },
  },
});
```

## Netlify Output Directories

The `netlify` and `netlify_edge` presets now write their output to the [Netlify Frameworks API](https://docs.netlify.com/build/frameworks/frameworks-api/) directories:

| Output                           | Before                           | After                        |
|----------------------------------|----------------------------------|------------------------------|
| Server function (`netlify`)      | `.netlify/functions-internal`    | `.netlify/v1/functions`      |
| Edge function (`netlify_edge`)   | `.netlify/edge-functions`        | `.netlify/v1/edge-functions` |
| Deploy config (`netlify.config`) | `.netlify/deploy/v1/config.json` | `.netlify/v1/config.json`    |

The edge function config is now inlined in `server.mjs` instead of `.netlify/edge-functions/manifest.json`, and `netlify.config` is also applied by the `netlify_edge` preset.

**Migration:**

- Update CI scripts or cache paths that reference the old directories.
- When deploying a local build with `netlify deploy --no-build`, remove leftovers from older builds in `.netlify/functions-internal` and `.netlify/edge-functions` first.

## Cloudflare Pages Redirect Targets

The Cloudflare Pages presets no longer prefix the [`baseURL`](/config#baseurl) onto relative `redirect` targets in the generated `_redirects` file. Targets are now used as written, like in the server runtime and on the Vercel and Netlify presets. Route rule keys (the matched paths) are still relative to `baseURL`. The EdgeOne preset, which also prefixed targets in earlier v3 prereleases, follows the same behavior.

**Migration:**

If you use a non-root `baseURL`, include it in relative redirect targets:

```diff [nitro.config.ts]
export default defineConfig({
  baseURL: "/app/",
  routeRules: {
--    "/old/**": { redirect: "/new/**" },
++    "/old/**": { redirect: "/app/new/**" },
  },
});
```

## Cloudflare Bindings Access

In Nitro v2, Cloudflare environment variables and bindings were accessible via `event.context.cloudflare.env`.

In Nitro v3, the Cloudflare runtime context is attached to the request's runtime object instead.

**Migration:**

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

## Cloudflare Email and Queue Hooks

The deprecated `event` field is removed from the `cloudflare:email` and `cloudflare:queue` runtime hooks. Use `message` and `batch` instead.

**Migration:**

```diff
-- nitroApp.hooks.hook("cloudflare:email", ({ event }) => {})
++ nitroApp.hooks.hook("cloudflare:email", ({ message }) => {})
-- nitroApp.hooks.hook("cloudflare:queue", ({ event }) => {})
++ nitroApp.hooks.hook("cloudflare:queue", ({ batch }) => {})
```

## Cloudflare Durable Preset Is Deprecated

The `cloudflare_durable` preset is merged into `cloudflare_module`. The Worker entry exports the same `$DurableObject` class and forwards WebSocket upgrades to it when its binding is declared in your Wrangler configuration. See [WebSockets with Durable Objects](/deploy/providers/cloudflare#websockets-with-durable-objects).

**Migration:**

- Keep your Wrangler configuration and switch the preset to `cloudflare_module`. The `cloudflare_durable` preset still works but logs a deprecation warning.

```diff [nitro.config.ts]
export default defineConfig({
-- preset: "cloudflare_durable",
++ preset: "cloudflare_module",
});
```

## Cloudflare Exports

Earlier v3 prereleases experimentally supported exporting Durable Objects, Workflows and other Cloudflare classes from an `exports.cloudflare.ts` file (or the `cloudflare.exports` option). They are now exported from your [`server.ts`](/docs/server-entry) file instead, and also work in local development. See [Durable Objects](/deploy/providers/cloudflare#durable-objects).

```diff [server.ts]
++ export { MyWorkflow } from "./exports.cloudflare.ts";
++
++ export default {
++   fetch() {},
++ };
```

## Vite Plugin Options

Earlier v3 prereleases accepted `services`, `assetsImport` and `serverReload` only as `experimental.vite` options passed to the `nitro()` Vite plugin. They moved to the top-level [`vite`](/config#vite) option, which can also be set under `nitro` in the Vite config or in `nitro.config.ts`. `experimental.vite` still works but is deprecated. The deprecated `ServiceConfig` type is removed in favor of `ViteServiceConfig`.

```diff [vite.config.ts]
-- nitro({ experimental: { vite: { services: { api: { entry: "./api.ts" } } } } })
++ nitro({ vite: { services: { api: { entry: "./api.ts" } } } })
```

## Removed Deprecated Options

Some options and exports that were deprecated or had no effect in earlier v3 prereleases are removed:

- `netlify.images`: use `netlify.config.images`.
- `vercel.regions`: it only applied to the removed `vercel-edge` preset. Use `vercel.functions.regions`.
- `prerender.retry` and `prerender.retryDelay`: they had no effect in v3.
- `useKV` from `nitro/storage`: import it from `nitro/kv`.
- `getBuildInfo(rootDir)` from `nitro/builder`: pass an options object, `getBuildInfo({ rootDir })`.

---

### 1.Docs 99.Nightly (`docs/1.docs/99.nightly.md`)

---
icon: ri:moon-fill
---

# Nightly Channel

> Try the latest Nitro changes before they are released, with builds published automatically for every commit to the `main` branch.

You can opt in to the nightly release channel by updating your `package.json`:

```json
{
  "devDependencies": {
    "nitro": "npm:nitro-nightly@latest"
  }
}
```

Remove the lockfile (`package-lock.json`, `yarn.lock`, `pnpm-lock.yaml`, `bun.lock`, or `bun.lockb`) and reinstall the dependencies.

When using **Bun as package manager** in a mono-repo, you need to make sure nitro package is properly hoisted:

```toml [bunfig.toml]
[install]
publicHoistPattern = ["nitro*"]
```

Avoid using `<npm|pnpm|yarn|bun|deno> install nitro-nightly`; it does not install correctly.
If you encounter issues, delete your `node_modules` and lock files, then follow the steps above.

Migrating from Nitro v2 at the same time? See the [migration guide](/docs/migration).

---

### 4.Examples Api Routes (`docs/4.examples/api-routes.md`)

---
navigation:
  category: features
icon: i-lucide-route
---

# API Routes

> File-based API routing with HTTP method support and dynamic parameters.



::code-tree{defaultValue="api/hello.ts" expandAll}

```html [index.html]
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width,initial-scale=1" />
    <title>API Routes</title>
  </head>
  <body>
    <h2>API Routes:</h2>
    <ul>
      <li><a href="/api/hello">/api/hello</a></li>
      <li><a href="/api/hello/world">/api/hello/world</a></li>
      <li><a href="/api/test">/api/test</a></li>
    </ul>
  </body>
</html>
```

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

export default defineConfig({
  serverDir: "./",
});
```

```json [package.json]
{
  "type": "module",
  "scripts": {
    "dev": "nitro dev",
    "build": "nitro build"
  },
  "devDependencies": {
    "nitro": "latest"
  }
}
```

```json [tsconfig.json]
{
  "extends": "nitro/tsconfig"
}
```

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

export default defineConfig({ plugins: [nitro()] });
```

```ts [api/hello.ts]
import { defineHandler } from "nitro";

export default defineHandler(() => "Nitro is amazing!");
```

```ts [api/test.get.ts]
import { defineHandler } from "nitro";

export default defineHandler(() => "Test get handler");
```

```ts [api/test.post.ts]
import { defineHandler } from "nitro";

export default defineHandler(async (event) => {
  const body = await event.req.json();
  return {
    message: "Test post handler",
    body,
  };
});
```

```ts [api/hello/[name].ts]
import { defineHandler } from "nitro";

export default defineHandler((event) => `Hello (param: ${event.context.params!.name})!`);
```

::





Nitro supports file-based routing in the `api/` or `routes/` directory. Each file becomes an API endpoint based on its path.

## Basic Route

Create a file in the `api/` directory to define a route. The file path becomes the URL path:

```ts [api/hello.ts]
import { defineHandler } from "nitro";

export default defineHandler(() => "Nitro is amazing!");
```

This creates a `GET /api/hello` endpoint.

## Dynamic Routes

Use square brackets `[param]` for dynamic URL segments. Access params via `event.context.params`:

```ts [api/hello/[name].ts]
import { defineHandler } from "nitro";

export default defineHandler((event) => `Hello (param: ${event.context.params!.name})!`);
```

This creates a `GET /api/hello/:name` endpoint (e.g., `/api/hello/world`).

## HTTP Methods

Suffix your file with the HTTP method (`.get.ts`, `.post.ts`, `.put.ts`, `.delete.ts`, etc.):

### GET Handler

```ts [api/test.get.ts]
import { defineHandler } from "nitro";

export default defineHandler(() => "Test get handler");
```

### POST Handler

```ts [api/test.post.ts]
import { defineHandler } from "nitro";

export default defineHandler(async (event) => {
  const body = await event.req.json();
  return {
    message: "Test post handler",
    body,
  };
});
```



## Learn More

- [Routing](/docs/routing)

---

### 4.Examples Cached Handler (`docs/4.examples/cached-handler.md`)

---
navigation:
  category: features
icon: i-lucide-clock
---

# Cached Handler

> Cache route responses with configurable bypass logic.



::code-tree{defaultValue="server.ts" expandAll}

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

export default defineConfig({});
```

```json [package.json]
{
  "type": "module",
  "scripts": {
    "dev": "nitro dev",
    "build": "nitro build"
  },
  "devDependencies": {
    "nitro": "latest"
  }
}
```

```ts [server.ts]
import { html } from "nitro";
import { defineCachedHandler } from "nitro/cache";

export default defineCachedHandler(
  async () => {
    await new Promise((resolve) => setTimeout(resolve, 500));
    return html`
      Response generated at ${new Date().toISOString()} (took 500ms)
      <br />(<a href="?skipCache=true">skip cache</a>)
    `;
  },
  { shouldBypassCache: ({ req }) => req.url.includes("skipCache=true") }
);
```

```json [tsconfig.json]
{
  "extends": "nitro/tsconfig"
}
```

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

export default defineConfig({ plugins: [nitro()] });
```

::





This example shows how to cache an expensive operation (a 500 ms delay) and conditionally bypass the cache using a query parameter. On first request, the handler executes and caches the result. Subsequent requests return the cached response instantly until the cache expires or is bypassed.

## How It Works

```ts [server.ts]
import { html } from "nitro";
import { defineCachedHandler } from "nitro/cache";

export default defineCachedHandler(
  async () => {
    await new Promise((resolve) => setTimeout(resolve, 500));
    return html`
      Response generated at ${new Date().toISOString()} (took 500ms)
      <br />(<a href="?skipCache=true">skip cache</a>)
    `;
  },
  { shouldBypassCache: ({ req }) => req.url.includes("skipCache=true") }
);
```

The handler simulates a slow operation with a 500ms delay. As `defineCachedHandler` wraps it, the response is cached after the first execution. The `shouldBypassCache` option checks for `?skipCache=true` in the URL and when present the cache is skipped and the handler runs fresh.



## Learn More

- [Cache](/docs/cache)
- [Storage](/docs/storage)

---

### 4.Examples Custom Error Handler (`docs/4.examples/custom-error-handler.md`)

---
navigation:
  category: features
icon: i-lucide-alert-circle
---

# Custom Error Handler

> Customize error responses with a global error handler.



::code-tree{defaultValue="error.ts" expandAll}

```ts [error.ts]
import { defineErrorHandler } from "nitro";

export default defineErrorHandler((error, _event) => {
  return new Response(`Custom Error Handler: ${error.message}`, {
    status: 500,
    headers: { "Content-Type": "text/plain" },
  });
});
```

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

export default defineConfig({
  errorHandler: "./error.ts",
  // devErrorHandler: errorHandler,
});
```

```json [package.json]
{
  "type": "module",
  "scripts": {
    "dev": "nitro dev",
    "build": "nitro build"
  },
  "devDependencies": {
    "nitro": "latest"
  }
}
```

```ts [server.ts]
import { defineHandler, HTTPError } from "nitro";

export default defineHandler(() => {
  throw new HTTPError("Example Error!", { status: 500 });
});
```

```json [tsconfig.json]
{
  "extends": "nitro/tsconfig"
}
```

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

export default defineConfig({ plugins: [nitro()] });
```

::





This example shows how to intercept all errors and return a custom response format. When any route throws an error, Nitro calls your error handler instead of returning the default error page.

## Error Handler

Create an `error.ts` file in your project root to define the global error handler:

```ts [error.ts]
import { defineErrorHandler } from "nitro";

export default defineErrorHandler((error, _event) => {
  return new Response(`Custom Error Handler: ${error.message}`, {
    status: 500,
    headers: { "Content-Type": "text/plain" },
  });
});
```

The handler receives the thrown error and the H3 event object. You can use the event to access request details like headers, cookies, or the URL path to customize responses per route.

## Triggering an Error

The main handler throws an error to demonstrate the custom error handler:

```ts [server.ts]
import { defineHandler, HTTPError } from "nitro";

export default defineHandler(() => {
  throw new HTTPError("Example Error!", { status: 500 });
});
```

When you visit the page, instead of seeing a generic error page, you'll see "Custom Error Handler: Example Error!" because the error handler intercepts the thrown error.



## Learn More

- [Server Entry](/docs/server-entry)

---

### 4.Examples Database (`docs/4.examples/database.md`)

---
navigation:
  category: features
icon: i-lucide-database
---

# Database

> Built-in database support with SQL template literals.



::code-tree{defaultValue="server.ts" expandAll}

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

export default defineConfig({
  experimental: {
    database: true,
    tasks: true,
  },
  database: {
    default: { connector: "sqlite" },
  },
});
```

```json [package.json]
{
  "type": "module",
  "scripts": {
    "dev": "nitro dev",
    "build": "nitro build"
  },
  "dependencies": {
    "db0": "^0.4.1"
  },
  "devDependencies": {
    "nitro": "latest"
  }
}
```

```ts [server.ts]
import { defineHandler } from "nitro";
import { useDatabase } from "nitro/database";

export default defineHandler(async () => {
  const db = useDatabase();

  // Create users table
  await db.sql`DROP TABLE IF EXISTS users`;
  await db.sql`CREATE TABLE IF NOT EXISTS users ("id" TEXT PRIMARY KEY, "firstName" TEXT, "lastName" TEXT, "email" TEXT)`;

  // Add a new user
  const userId = String(Math.round(Math.random() * 10_000));
  await db.sql`INSERT INTO users VALUES (${userId}, 'John', 'Doe', '')`;

  // Query for users
  const { rows } = await db.sql`SELECT * FROM users WHERE id = ${userId}`;

  return {
    rows,
  };
});
```

```json [tsconfig.json]
{
  "extends": "nitro/tsconfig"
}
```

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

export default defineConfig({ plugins: [nitro()] });
```

```ts [tasks/db/migrate.ts]
import { defineTask } from "nitro/task";
import { useDatabase } from "nitro/database";

export default defineTask({
  meta: {
    description: "Run database migrations",
  },
  async run() {
    const db = useDatabase();

    console.log("Running database migrations...");

    // Create users table
    await db.sql`DROP TABLE IF EXISTS users`;
    await db.sql`CREATE TABLE IF NOT EXISTS users ("id" TEXT PRIMARY KEY, "firstName" TEXT, "lastName" TEXT, "email" TEXT)`;

    return {
      result: "Database migrations complete!",
    };
  },
});
```

::





Nitro provides a built-in database layer that uses SQL template literals for safe, parameterized queries. This example creates a users table, inserts a record, and queries it back.

## Querying the Database

```ts [server.ts]
import { defineHandler } from "nitro";
import { useDatabase } from "nitro/database";

export default defineHandler(async () => {
  const db = useDatabase();

  // Create users table
  await db.sql`DROP TABLE IF EXISTS users`;
  await db.sql`CREATE TABLE IF NOT EXISTS users ("id" TEXT PRIMARY KEY, "firstName" TEXT, "lastName" TEXT, "email" TEXT)`;

  // Add a new user
  const userId = String(Math.round(Math.random() * 10_000));
  await db.sql`INSERT INTO users VALUES (${userId}, 'John', 'Doe', '')`;

  // Query for users
  const { rows } = await db.sql`SELECT * FROM users WHERE id = ${userId}`;

  return {
    rows,
  };
});
```

Retrieve the database instance using `useDatabase()`. The database can be queried using `db.sql`, and variables like `${userId}` are automatically escaped to prevent SQL injection.

## Running Migrations with Tasks

Nitro tasks let you run operations outside of request handlers. For database migrations, create a task file in `tasks/` and run it via the CLI. This keeps schema changes separate from your application code.

```ts [tasks/db/migrate.ts]
import { defineTask } from "nitro/task";
import { useDatabase } from "nitro/database";

export default defineTask({
  meta: {
    description: "Run database migrations",
  },
  async run() {
    const db = useDatabase();

    console.log("Running database migrations...");

    // Create users table
    await db.sql`DROP TABLE IF EXISTS users`;
    await db.sql`CREATE TABLE IF NOT EXISTS users ("id" TEXT PRIMARY KEY, "firstName" TEXT, "lastName" TEXT, "email" TEXT)`;

    return {
      result: "Database migrations complete!",
    };
  },
});
```



## Learn More

- [Database](/docs/database)
- [Tasks](/docs/tasks)

---

### 4.Examples Elysia (`docs/4.examples/elysia.md`)

---
navigation:
  category: backend frameworks
icon: i-skill-icons-elysia-dark
---

# Elysia

> Integrate Elysia with Nitro using the server entry.



::code-tree{defaultValue="server.ts" expandAll}

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

export default defineConfig({});
```

```json [package.json]
{
  "type": "module",
  "scripts": {
    "build": "nitro build",
    "dev": "nitro dev"
  },
  "devDependencies": {
    "elysia": "^1.4.30",
    "nitro": "latest"
  }
}
```

```ts [server.ts]
import { Elysia } from "elysia";

const app = new Elysia();

app.get("/", () => "Hello, Elysia with Nitro!");

export default app.compile();
```

```json [tsconfig.json]
{
  "extends": "nitro/tsconfig"
}
```

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

export default defineConfig({ plugins: [nitro()] });
```

::





## Server Entry

```ts [server.ts]
import { Elysia } from "elysia";

const app = new Elysia();

app.get("/", () => "Hello, Elysia with Nitro!");

export default app.compile();
```

Nitro auto-detects `server.ts` in your project root and uses it as the server entry. The Elysia app handles all incoming requests, giving you full control over routing and middleware.

Call `app.compile()` before exporting to optimize the router for production.



## Learn More

- [Server Entry](/docs/server-entry)
- [Elysia Documentation](https://elysiajs.com/)

---

### 4.Examples Express (`docs/4.examples/express.md`)

---
navigation:
  category: backend frameworks
icon: i-simple-icons-express
---

# Express

> Integrate Express with Nitro using the server entry.



::code-tree{defaultValue="server.node.ts" expandAll}

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

export default defineConfig({});
```

```json [package.json]
{
  "type": "module",
  "scripts": {
    "build": "nitro build",
    "dev": "nitro dev"
  },
  "devDependencies": {
    "@types/express": "^5.0.6",
    "express": "^5.3.0",
    "nitro": "latest"
  }
}
```

```ts [server.node.ts]
import Express from "express";

const app = Express();

app.use("/", (_req, res) => {
  res.send("Hello from Express with Nitro!");
});

export default app;
```

```json [tsconfig.json]
{
  "extends": "nitro/tsconfig"
}
```

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

export default defineConfig({ plugins: [nitro()] });
```

::





## Server Entry

```ts [server.node.ts]
import Express from "express";

const app = Express();

app.use("/", (_req, res) => {
  res.send("Hello from Express with Nitro!");
});

export default app;
```

Nitro auto-detects `server.node.ts` in your project root and uses it as the server entry. The Express app handles all incoming requests, giving you full control over routing and middleware.

::note
The `.node.ts` suffix indicates this entry is Node.js specific and won't work in other runtimes like Cloudflare Workers or Deno.
::



## Learn More

- [Server Entry](/docs/server-entry)
- [Express Documentation](https://expressjs.com/)

---

### 4.Examples Fastify (`docs/4.examples/fastify.md`)

---
navigation:
  category: backend frameworks
icon: i-simple-icons-fastify
---

# Fastify

> Integrate Fastify with Nitro using the server entry.



::code-tree{defaultValue="server.node.ts" expandAll}

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

export default defineConfig({});
```

```json [package.json]
{
  "type": "module",
  "scripts": {
    "build": "nitro build",
    "dev": "nitro dev"
  },
  "devDependencies": {
    "fastify": "^5.12.7",
    "nitro": "latest"
  }
}
```

```ts [server.node.ts]
import Fastify from "fastify";

const app = Fastify();

app.get("/", () => "Hello, Fastify with Nitro!");

await app.ready();

export default app.routing;
```

```json [tsconfig.json]
{
  "extends": "nitro/tsconfig"
}
```

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

export default defineConfig({ plugins: [nitro()] });
```

::





## Server Entry

```ts [server.node.ts]
import Fastify from "fastify";

const app = Fastify();

app.get("/", () => "Hello, Fastify with Nitro!");

await app.ready();

export default app.routing;
```

Nitro auto-detects `server.node.ts` in your project root and uses it as the server entry.

Call `await app.ready()` to initialize all registered plugins before exporting. Export `app.routing` (not `app`) to provide Nitro with the request handler function.

::note
The `.node.ts` suffix indicates this entry is Node.js specific and won't work in other runtimes like Cloudflare Workers or Deno.
::



## Learn More

- [Server Entry](/docs/server-entry)
- [Fastify Documentation](https://fastify.dev/)

---

### 4.Examples Hello World (`docs/4.examples/hello-world.md`)

---
navigation:
  category: features
icon: i-lucide-sparkles
---

# Hello World

> Minimal Nitro server using the web standard fetch handler.



::code-tree{defaultValue="server.ts" expandAll}

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

export default defineConfig({});
```

```json [package.json]
{
  "type": "module",
  "scripts": {
    "build": "nitro build",
    "dev": "nitro dev",
    "preview": "node .output/server/index.mjs"
  },
  "devDependencies": {
    "nitro": "latest"
  }
}
```

```ts [server.ts]
export default {
  fetch(req: Request) {
    return new Response("Nitro Works!");
  },
};
```

```json [tsconfig.json]
{
  "extends": "nitro/tsconfig"
}
```

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

export default defineConfig({ plugins: [nitro()] });
```

::





The simplest Nitro server. Export an object with a `fetch` method that receives a standard `Request` and returns a `Response`. No frameworks, no abstractions, just the web platform.


## Server Entry

```ts [server.ts]
export default {
  fetch(req: Request) {
    return new Response("Nitro Works!");
  },
};
```

The `fetch` method follows the same signature as Service Workers and Cloudflare Workers. This pattern works across all deployment targets because it uses web standards.

Add the Nitro plugin to Vite and it handles the rest: dev server, hot reloading, and production builds.



## Learn More

- [Server Entry](/docs/server-entry)
- [Configuration](/docs/configuration)

---

### 4.Examples Hono (`docs/4.examples/hono.md`)

---
navigation:
  category: backend frameworks
icon: i-logos-hono
---

# Hono

> Integrate Hono with Nitro using the server entry.



::code-tree{defaultValue="server.ts" expandAll}

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

export default defineConfig({});
```

```json [package.json]
{
  "type": "module",
  "scripts": {
    "build": "nitro build",
    "dev": "nitro dev"
  },
  "devDependencies": {
    "hono": "^4.13.13",
    "nitro": "latest"
  }
}
```

```ts [server.ts]
import { Hono } from "hono";

const app = new Hono();

app.get("/", (c) => {
  return c.text("Hello, Hono with Nitro!");
});

export default app;
```

```json [tsconfig.json]
{
  "extends": "nitro/tsconfig"
}
```

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

export default defineConfig({ plugins: [nitro()] });
```

::





## Server Entry

```ts [server.ts]
import { Hono } from "hono";

const app = new Hono();

app.get("/", (c) => {
  return c.text("Hello, Hono with Nitro!");
});

export default app;
```

Nitro auto-detects `server.ts` in your project root and uses it as the server entry. The Hono app handles all incoming requests, giving you full control over routing and middleware.

Hono is cross-runtime compatible, so this server entry works across all Nitro deployment targets including Node.js, Deno, Bun, and Cloudflare Workers.



## Learn More

- [Server Entry](/docs/server-entry)
- [Hono Documentation](https://hono.dev/)

---

### 4.Examples Import Alias (`docs/4.examples/import-alias.md`)

---
navigation:
  category: config
icon: i-lucide-at-sign
---

# Import Alias

> Custom import aliases for cleaner module paths.



::code-tree{defaultValue="server/routes/index.ts" expandAll}

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

export default defineConfig({
  serverDir: true,
});
```

```json [package.json]
{
  "type": "module",
  "imports": {
    "#server/*": "./server/*"
  },
  "scripts": {
    "build": "nitro build",
    "dev": "nitro dev",
    "preview": "node .output/server/index.mjs"
  },
  "devDependencies": {
    "nitro": "latest"
  }
}
```

```json [tsconfig.json]
{
  "extends": "nitro/tsconfig",
  "compilerOptions": {
    "paths": {
      "~server/*": ["./server/*"]
    }
  }
}
```

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

export default defineConfig({ plugins: [nitro()] });
```

```ts [server/routes/index.ts]
import { sum } from "~server/utils/math.ts";

import { rand } from "#server/utils/math.ts";

export default () => {
  const [a, b] = [rand(1, 10), rand(1, 10)];
  const result = sum(a, b);
  return `The sum of ${a} + ${b} = ${result}`;
};
```

```ts [server/utils/math.ts]
export function rand(min: number, max: number): number {
  return Math.floor(Math.random() * (max - min + 1)) + min;
}

export function sum(a: number, b: number): number {
  return a + b;
}
```

::





Import aliases like `~` and `#` let you reference modules with shorter paths instead of relative imports.

## Importing Using Aliases

```ts [server/routes/index.ts]
import { sum } from "~server/utils/math.ts";

import { rand } from "#server/utils/math.ts";

export default () => {
  const [a, b] = [rand(1, 10), rand(1, 10)];
  const result = sum(a, b);
  return `The sum of ${a} + ${b} = ${result}`;
};
```

The route imports the `sum` function using `~server/` and `rand` using `#server/`. Both resolve to the same `server/utils/math.ts` file. The handler generates two random numbers and returns their sum.

## Configuration

Aliases can be configured in `package.json` imports field, `compilerOptions.paths` of `tsconfig.json` or the `alias` option of `nitro.config.ts`.



## Learn More

- [Configuration](/docs/configuration)

---

### 4.Examples Middleware (`docs/4.examples/middleware.md`)

---
navigation:
  category: features
icon: i-lucide-layers
---

# Middleware

> Request middleware for authentication, logging, and request modification.



::code-tree{defaultValue="server/middleware/auth.ts" expandAll}

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

export default defineConfig({
  serverDir: true,
});
```

```json [package.json]
{
  "type": "module",
  "scripts": {
    "dev": "nitro dev",
    "build": "nitro build"
  },
  "devDependencies": {
    "nitro": "latest"
  }
}
```

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

export default defineHandler((event) => ({
  auth: event.context.auth,
}));
```

```json [tsconfig.json]
{
  "extends": "nitro/tsconfig"
}
```

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

export default defineConfig({ plugins: [nitro()] });
```

```ts [server/middleware/auth.ts]
import { defineMiddleware } from "nitro";

export default defineMiddleware((event) => {
  event.context.auth = { name: "User " + Math.round(Math.random() * 100) };
});
```

::





Middleware functions run before route handlers on every request. They can modify the request, add context, or return early responses.

## Defining Middleware

Create files in `server/middleware/`. They run in alphabetical order:

```ts [server/middleware/auth.ts]
import { defineMiddleware } from "nitro";

export default defineMiddleware((event) => {
  event.context.auth = { name: "User " + Math.round(Math.random() * 100) };
});
```

Middleware can:
- Add data to `event.context` for use in handlers
- Return a response early to short-circuit the request
- Modify request headers or other properties

## Accessing Context in Handlers

Data added to `event.context` in middleware is available in all subsequent handlers:

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

export default defineHandler((event) => ({
  auth: event.context.auth,
}));
```



## Learn More

- [Routing](/docs/routing)

---

### 4.Examples Mono Jsx (`docs/4.examples/mono-jsx.md`)

---
navigation:
  category: server side rendering
icon: i-lucide-brackets
---

# Mono JSX

> Server-side JSX rendering in Nitro with mono-jsx.



::code-tree{defaultValue="server.tsx" expandAll}

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

export default defineConfig({});
```

```json [package.json]
{
  "type": "module",
  "scripts": {
    "dev": "nitro dev",
    "build": "nitro build"
  },
  "devDependencies": {
    "mono-jsx": "latest",
    "nitro": "latest"
  }
}
```

```tsx [server.tsx]
export default () => (
  <html>
    <h1>Nitro + mono-jsx works!</h1>
  </html>
);
```

```json [tsconfig.json]
{
  "extends": "nitro/tsconfig",
  "compilerOptions": {
    "jsx": "react-jsx",
    "jsxImportSource": "mono-jsx"
  }
}
```

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

export default defineConfig({ plugins: [nitro()] });
```

::





## Server Entry

```tsx [server.tsx]
export default () => (
  <html>
    <h1>Nitro + mono-jsx works!</h1>
  </html>
);
```

Nitro auto-detects `server.tsx` and uses mono-jsx to transform JSX into HTML. Export a function that returns JSX, and Nitro sends the rendered HTML as the response.



## Learn More

- [Renderer](/docs/renderer)
- [mono-jsx](https://github.com/aspect-dev/mono-jsx)

---

### 4.Examples Nano Jsx (`docs/4.examples/nano-jsx.md`)

---
navigation:
  category: server side rendering
icon: i-lucide-brackets
---

# Nano JSX

> Server-side JSX rendering in Nitro with nano-jsx.



::code-tree{defaultValue="server.tsx" expandAll}

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

export default defineConfig({});
```

```json [package.json]
{
  "type": "module",
  "scripts": {
    "dev": "nitro dev",
    "build": "nitro build"
  },
  "devDependencies": {
    "nano-jsx": "^0.2.1",
    "nitro": "latest"
  }
}
```

```tsx [server.tsx]
import { defineHandler, html } from "nitro";
import { renderSSR } from "nano-jsx";

export default defineHandler(() => {
  return html(renderSSR(() => <h1>Nitro + nano-jsx works!</h1>));
});
```

```json [tsconfig.json]
{
  "extends": "nitro/tsconfig",
  "compilerOptions": {
    "jsx": "react-jsx",
    "jsxImportSource": "nano-jsx/esm"
  }
}
```

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

export default defineConfig({ plugins: [nitro()] });
```

::





## Server Entry

```tsx [server.tsx]
import { defineHandler, html } from "nitro";
import { renderSSR } from "nano-jsx";

export default defineHandler(() => {
  return html(renderSSR(() => <h1>Nitro + nano-jsx works!</h1>));
});
```

Nitro auto-detects `server.tsx` and uses it as the server entry. Use `renderSSR` from nano-jsx to convert JSX into an HTML string. The `html` helper from H3 sets the correct content type header.



## Learn More

- [Renderer](/docs/renderer)
- [nano-jsx](https://nanojsx.io/)

---

### 4.Examples Plugins (`docs/4.examples/plugins.md`)

---
navigation:
  category: features
icon: i-lucide-plug
---

# Plugins

> Extend Nitro with custom plugins for hooks and lifecycle events.



::code-tree{defaultValue="server/plugins/test.ts" expandAll}

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

export default defineConfig({
  serverDir: true,
});
```

```json [package.json]
{
  "type": "module",
  "scripts": {
    "dev": "nitro dev",
    "build": "nitro build"
  },
  "devDependencies": {
    "nitro": "latest"
  }
}
```

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

export default defineHandler(() => "<h1>Hello Nitro!</h1>");
```

```json [tsconfig.json]
{
  "extends": "nitro/tsconfig"
}
```

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

export default defineConfig({ plugins: [nitro()] });
```

```ts [server/plugins/test.ts]
import { definePlugin } from "nitro";
import { useNitroHooks } from "nitro/app";

export default definePlugin(() => {
  const hooks = useNitroHooks();
  hooks.hook("response", (res) => {
    res.headers.set("content-type", "html; charset=utf-8");
  });
});
```

::





Plugins let you hook into Nitro's runtime lifecycle. This example shows a plugin that modifies the `Content-Type` header on every response. Create files in `server/plugins/` and they're automatically loaded at startup.

## Defining a Plugin

```ts [server/plugins/test.ts]
import { definePlugin } from "nitro";
import { useNitroHooks } from "nitro/app";

export default definePlugin(() => {
  const hooks = useNitroHooks();
  hooks.hook("response", (res) => {
    res.headers.set("content-type", "html; charset=utf-8");
  });
});
```

The plugin uses `useNitroHooks()` to access the hooks system, then registers a `response` hook that runs after every request and receives the outgoing `Response`. Here it sets the content type to HTML, but you could log requests, add security headers, or modify responses in any way.

## Main Handler

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

export default defineHandler(() => "<h1>Hello Nitro!</h1>");
```

The handler returns HTML without setting a content type. The plugin automatically adds the correct `Content-Type: html; charset=utf-8` header to the response.



## Learn More

- [Plugins](/docs/plugins)
- [Lifecycle](/docs/lifecycle)

---

### 4.Examples Renderer (`docs/4.examples/renderer.md`)

---
navigation:
  category: server side rendering
icon: i-lucide-code
---

# Custom Renderer

> Build a custom HTML renderer in Nitro with server-side data fetching.



::code-tree{defaultValue="renderer.ts" expandAll}

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

export default defineConfig({
  serverDir: "./",
  renderer: { handler: "./renderer" },
});
```

```json [package.json]
{
  "type": "module",
  "scripts": {
    "dev": "nitro dev",
    "build": "nitro build"
  },
  "devDependencies": {
    "nitro": "latest"
  }
}
```

```ts [renderer.ts]
import { fetch } from "nitro";

export default async function renderer({ url }: { req: Request; url: URL }) {
  const apiRes = await fetch("/api/hello").then((res) => res.text());
  return new Response(
    /* html */ `<!DOCTYPE html>
    <html>
    <head>
      <title>Custom Renderer</title>
    </head>
    <body>
      <h1>Hello from custom renderer!</h1>
      <p>Current path: ${url.pathname}</p>
      <p>API says: ${apiRes}</p>
    </body>
    </html>`,
    { headers: { "content-type": "text/html; charset=utf-8" } }
  );
}
```

```json [tsconfig.json]
{
  "extends": "nitro/tsconfig"
}
```

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

export default defineConfig({ plugins: [nitro()] });
```

```ts [api/hello.ts]
import { defineHandler } from "nitro";

export default defineHandler(() => "Nitro is amazing!");
```

::





Create a custom renderer that generates HTML responses with data from API routes. Use Nitro's internal `fetch` to call routes without network overhead.

## Renderer

```ts [renderer.ts]
import { fetch } from "nitro";

export default async function renderer({ url }: { req: Request; url: URL }) {
  const apiRes = await fetch("/api/hello").then((res) => res.text());
  return new Response(
    /* html */ `<!DOCTYPE html>
    <html>
    <head>
      <title>Custom Renderer</title>
    </head>
    <body>
      <h1>Hello from custom renderer!</h1>
      <p>Current path: ${url.pathname}</p>
      <p>API says: ${apiRes}</p>
    </body>
    </html>`,
    { headers: { "content-type": "text/html; charset=utf-8" } }
  );
}
```

Configure the renderer with `renderer: { handler: "./renderer" }` in `nitro.config.ts`, and Nitro uses it for all non-API routes. The renderer function receives the request URL and returns a `Response`. (Alternatively, if an `index.html` exists in your project root, Nitro auto-detects it as a renderer template.)

Use `fetch` from `nitro` to call API routes without network overhead—these requests stay in-process.

## API Route

```ts [api/hello.ts]
import { defineHandler } from "nitro";

export default defineHandler(() => "Nitro is amazing!");
```

Define API routes in the `api/` directory. When the renderer calls `fetch("/api/hello")`, this handler runs and returns its response.



## Learn More

- [Renderer](/docs/renderer)

---

### 4.Examples Runtime Config (`docs/4.examples/runtime-config.md`)

---
navigation:
  category: config
icon: i-lucide-settings
---

# Runtime Config

> Environment-aware configuration with runtime access.



::code-tree{defaultValue="nitro.config.ts" expandAll}

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

export default defineConfig({
  serverDir: "./",
  runtimeConfig: {
    apiKey: "",
  },
});
```

```json [package.json]
{
  "type": "module",
  "scripts": {
    "dev": "nitro dev",
    "build": "nitro build"
  },
  "devDependencies": {
    "nitro": "latest"
  }
}
```

```ts [server.ts]
import { defineHandler } from "nitro";
import { useRuntimeConfig } from "nitro/runtime-config";

export default defineHandler((event) => {
  const runtimeConfig = useRuntimeConfig();
  return { runtimeConfig };
});
```

```json [tsconfig.json]
{
  "extends": "nitro/tsconfig"
}
```

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

export default defineConfig({ plugins: [nitro()] });
```

::





Runtime config lets you define configuration values that can be overridden by environment variables at runtime.

## Define Config Schema

Declare your runtime config with default values in `nitro.config.ts`:

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

export default defineConfig({
  serverDir: "./",
  runtimeConfig: {
    apiKey: "",
  },
});
```

## Access at Runtime

Use `useRuntimeConfig` to access configuration values in your handlers:

```ts [server.ts]
import { defineHandler } from "nitro";
import { useRuntimeConfig } from "nitro/runtime-config";

export default defineHandler((event) => {
  const runtimeConfig = useRuntimeConfig();
  return { runtimeConfig };
});
```

## Environment Variables

Override config values via environment variables prefixed with `NITRO_`:

```sh [.env]
# NEVER COMMIT SENSITIVE DATA. THIS IS ONLY FOR DEMO PURPOSES.
NITRO_API_KEY=secret-api-key
```



## Learn More

- [Configuration](/docs/configuration)

---

### 4.Examples Server Fetch (`docs/4.examples/server-fetch.md`)

---
navigation:
  category: features
icon: i-lucide-arrow-right-left
---

# Server Fetch

> Internal server-to-server requests without network overhead.



::code-tree{defaultValue="routes/index.ts" expandAll}

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

export default defineConfig({
  serverDir: "./",
  hooks: {
    "dev:start": async () => {
      const res = await serverFetch("/hello");
      const text = await res.text();
      console.log("Fetched /hello in nitro module:", res.status, text);
    },
  },
});
```

```json [package.json]
{
  "type": "module",
  "scripts": {
    "dev": "nitro dev",
    "build": "nitro build"
  },
  "devDependencies": {
    "nitro": "latest"
  }
}
```

```json [tsconfig.json]
{
  "extends": "nitro/tsconfig"
}
```

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

export default defineConfig({ plugins: [nitro()] });
```

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

export default defineHandler(() => "Hello!");
```

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

export default defineHandler(() => fetch("/hello"));
```

::





When you need one route to call another, use Nitro's `fetch` function instead of the global fetch. It makes internal requests that stay in-process, avoiding network round-trips. The request never leaves the server.

## Main Route

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

export default defineHandler(() => fetch("/hello"));
```

The index route imports `fetch` from `nitro` (not the global fetch) and calls the `/hello` route. This request is handled internally without going through the network stack.

## Internal API Route

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

export default defineHandler(() => "Hello!");
```

A simple route that returns "Hello!". When the index route calls `fetch("/hello")`, this handler runs and its response is returned directly.



## Learn More

- [Routing](/docs/routing)

---

### 4.Examples Shiki (`docs/4.examples/shiki.md`)

---
navigation:
  category: integrations
icon: i-lucide-highlighter
---

# Shiki

> Server-side syntax highlighting in Nitro with Shiki.



::code-tree{defaultValue="api/highlight.ts" expandAll}

```html [index.html]
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width,initial-scale=1" />
    <title>Hello World Snippet</title>
    <link rel="stylesheet" href="styles.css" />
  </head>
  <body>
    <div class="card" role="region" aria-label="Code snippet">
      <div class="label">JavaScript</div>
      <script server>
        const hl = (code) =>
          serverFetch("/api/highlight", {
            method: "POST",
            body: code,
          });
      </script>
      <pre><code>{{{ hl(`console.log("💚 Simple is beautiful!");`) }}}</code></pre>
    </div>
  </body>
</html>
```

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

export default defineConfig({
  serverDir: "./",
});
```

```json [package.json]
{
  "type": "module",
  "scripts": {
    "dev": "vite dev",
    "build": "vite build"
  },
  "devDependencies": {
    "nitro": "latest",
    "shiki": "latest"
  }
}
```

```css [styles.css]
html,
body {
  height: 100%;
  margin: 0;
}
body {
  display: flex;
  align-items: center;
  justify-content: center;
  background: #f6f8fa;
  font-family:
    system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans",
    "Liberation Sans", sans-serif;
}
.card {
  text-align: left;
  background: #0b1220;
  color: #e6edf3;
  padding: 1rem;
  border-radius: 8px;
  box-shadow: 0 8px 24px rgba(2, 6, 23, 0.2);
  max-width: 90%;
  width: 520px;
}
.label {
  font-size: 12px;
  color: #9aa7b2;
  margin-bottom: 8px;
}
pre {
  margin: 0;
  font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, "Courier New", monospace;
  font-size: 14px;
  background: transparent;
  white-space: pre;
  overflow: auto;
}
```

```json [tsconfig.json]
{
  "extends": "nitro/tsconfig"
}
```

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

export default defineConfig({
  plugins: [nitro()],
});
```

```ts [api/highlight.ts]
import { createHighlighterCore } from "shiki/core";
import { createOnigurumaEngine } from "shiki/engine/oniguruma";

const highlighter = await createHighlighterCore({
  engine: createOnigurumaEngine(import("shiki/wasm")),
  themes: [await import("shiki/themes/vitesse-dark.mjs")],
  langs: [await import("shiki/langs/ts.mjs")],
});

export default async ({ req }: { req: Request }) => {
  const code = await req.text();
  const html = await highlighter.codeToHtml(code, {
    lang: "ts",
    theme: "vitesse-dark",
  });
  return new Response(html, {
    headers: { "Content-Type": "text/html; charset=utf-8" },
  });
};
```

::





Use Shiki for syntax highlighting with TextMate grammars. This example highlights code on the server using Nitro's server scripts feature, which runs JavaScript inside HTML files before sending the response.

## API Route

```ts [api/highlight.ts]
import { createHighlighterCore } from "shiki/core";
import { createOnigurumaEngine } from "shiki/engine/oniguruma";

const highlighter = await createHighlighterCore({
  engine: createOnigurumaEngine(import("shiki/wasm")),
  themes: [await import("shiki/themes/vitesse-dark.mjs")],
  langs: [await import("shiki/langs/ts.mjs")],
});

export default async ({ req }: { req: Request }) => {
  const code = await req.text();
  const html = await highlighter.codeToHtml(code, {
    lang: "ts",
    theme: "vitesse-dark",
  });
  return new Response(html, {
    headers: { "Content-Type": "text/html; charset=utf-8" },
  });
};
```

Create a Shiki highlighter with the Vitesse Dark theme and TypeScript language support. When the API receives a POST request, it reads the code from the request body and returns highlighted HTML.

## Server-Side Rendering

```html [index.html]
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width,initial-scale=1" />
    <title>Hello World Snippet</title>
    <link rel="stylesheet" href="styles.css" />
  </head>
  <body>
    <div class="card" role="region" aria-label="Code snippet">
      <div class="label">JavaScript</div>
      <script server>
        const hl = (code) =>
          serverFetch("/api/highlight", {
            method: "POST",
            body: code,
          });
      </script>
      <pre><code>{{{ hl(`console.log("💚 Simple is beautiful!");`) }}}</code></pre>
    </div>
  </body>
</html>
```

The `<script server>` tag runs on the server before the HTML is sent. It defines a helper function that calls the highlight API using `serverFetch`. The triple-brace syntax `{{{ }}}` outputs the result without escaping, so the highlighted HTML renders correctly.



## Learn More

- [Shiki](https://shiki.style/)

---

### 4.Examples Takumi (`docs/4.examples/takumi.md`)

---
navigation:
  category: integrations
icon: i-lucide-image
---

# Takumi

> Generate dynamic Open Graph images from a Nitro route using Takumi.



::code-tree{defaultValue="routes/og.png.ts" expandAll}

```
/* Detailed source-code truncated for AI context efficiency. */
```

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

export default defineConfig({
  serverDir: "./",
});
```

```json [package.json]
{
  "type": "module",
  "scripts": {
    "dev": "vite dev",
    "build": "vite build",
    "preview": "node .output/server/index.mjs"
  },
  "devDependencies": {
    "nitro": "latest",
    "takumi-js": "^2.17.1",
    "vite": "latest"
  }
}
```

```json [tsconfig.json]
{
  "extends": "nitro/tsconfig"
}
```

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

export default defineConfig({
  plugins: [nitro()],
});
```

```ts [routes/og.png.ts]
import { defineHandler } from "nitro";
import { container, text } from "takumi-js/helpers";
import ImageResponse from "takumi-js/response";

export default defineHandler(async ({ url }) => {
  const title = url.searchParams.get("title") ?? "Takumi + Nitro";
  const description = url.searchParams.get("description") ?? "Render OG images from a Nitro route.";

  const start = performance.now();

  const response = new ImageResponse(
    container({
      style: {
        width: "100%",
        height: "100%",
        display: "flex",
        flexDirection: "column",
        justifyContent: "center",
        padding: "64px",
        backgroundImage: "linear-gradient(to bottom right, #fff1f2, #fecdd3)",
      },
      children: [
        text(title, { fontSize: 72, fontWeight: 700, color: "#111827" }),
        text(description, { fontSize: 42, fontWeight: 500, color: "#4b5563" }),
      ],
    }),
    { width: 1200, height: 630 }
  );

  await response.ready;
  response.headers.set("Server-Timing", `render;dur=${(performance.now() - start).toFixed(1)}`);

  return response;
});
```

```
/* Detailed source-code truncated for AI context efficiency. */
```

::





Generate dynamic [Open Graph](https://ogp.me/) images from a Nitro route using [Takumi](https://takumi.kane.tw). The `index.html` page references the generated image through its `og:image` meta tag and previews it live.

## Server Route

Build the node tree with Takumi [helpers](https://takumi.kane.tw/docs/helpers) — no JSX setup needed. Nitro handlers can return a `Response`, so return an `ImageResponse` directly. The handler awaits `response.ready` and adds a `Server-Timing` header so callers can see how long the render took:

```ts [routes/og.png.ts]
import { defineHandler } from "nitro";
import { container, text } from "takumi-js/helpers";
import ImageResponse from "takumi-js/response";

export default defineHandler(async ({ url }) => {
  const title = url.searchParams.get("title") ?? "Takumi + Nitro";
  const description =
    url.searchParams.get("description") ?? "Render OG images from a Nitro route.";

  const start = performance.now();

  const response = new ImageResponse(
    container({
      style: {
        width: "100%",
        height: "100%",
        display: "flex",
        flexDirection: "column",
        justifyContent: "center",
        padding: "64px",
        backgroundImage: "linear-gradient(to bottom right, #fff1f2, #fecdd3)",
      },
      children: [
        text(title, { fontSize: 72, fontWeight: 700, color: "#111827" }),
        text(description, { fontSize: 42, fontWeight: 500, color: "#4b5563" }),
      ],
    }),
    { width: 1200, height: 630 },
  );

  await response.ready;
  response.headers.set("Server-Timing", `render;dur=${(performance.now() - start).toFixed(1)}`);

  return response;
});
```

## Referencing the Image

The `index.html` points its Open Graph tags at the route so crawlers get a freshly rendered preview:

```html [index.html]
<meta property="og:image" content="/og.png?title=Takumi%20%2B%20Nitro" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
```

## Request the Endpoint

Visit `/og.png?title=Hello&description=From%20Nitro` to render an image with custom text. The response includes a `Server-Timing` header reporting the render duration; the demo page re-fetches the image as you type in the title/description fields and shows a "Generating…" / "N ms" badge overlaid in the bottom-right corner of the preview. The link icon next to the title always points at the raw endpoint for the current preview.

The `index.html` head preloads the initial image so the browser starts fetching it in parallel with parsing, and the `<img>` reserves its `1200x630` aspect ratio via width/height attributes and CSS to avoid layout shift while it loads.

Takumi picks the render backend from the deployment target: native bindings on the Node preset, WebAssembly on edge presets. No config is needed.



## Learn More

- [Takumi](https://takumi.kane.tw)

---

### 4.Examples Tracing (`docs/4.examples/tracing.md`)

---
category: features
icon: i-lucide-activity
---

# Tracing

> Instrument the request lifecycle with diagnostics channels and the built-in tracing logger.



::code-tree{defaultValue="nitro.config.ts" expandAll}

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

export default defineConfig({
  serverDir: true,

  // Instrument the h3, srvx and unstorage tracing channels.
  tracingChannel: true,

  experimental: {
    // Log every completed span to the console (built-in, dependency-free sink).
    tracingLogger: true,
  },
});
```

```json [package.json]
{
  "type": "module",
  "scripts": {
    "build": "nitro build",
    "dev": "nitro dev",
    "preview": "node .output/server/index.mjs"
  },
  "dependencies": {
    "unstorage": "^2.0.0-beta.0"
  },
  "devDependencies": {
    "nitro": "latest"
  }
}
```

```json [tsconfig.json]
{
  "extends": "nitro/tsconfig"
}
```

```ts [server/middleware/timing.ts]
import { defineMiddleware } from "nitro";

// A trivial middleware — each request produces its own `middleware` span.
export default defineMiddleware((event) => {
  event.context.requestedAt = Date.now();
});
```

```ts [server/routes/index.ts]
import { defineHandler } from "nitro";
import { useKV } from "nitro/kv";

// Reads and writes storage so the request emits `unstorage.*` spans (CLIENT)
// alongside the `srvx.request`, `middleware` and route (`h3.request`) spans.
export default defineHandler(async () => {
  const storage = useKV();
  const hits = ((await storage.getItem<number>("hits")) ?? 0) + 1;
  await storage.setItem("hits", hits);
  return { message: "Hello from the Nitro tracing demo", hits };
});
```

```ts [server/routes/users/[id].ts]
import { defineHandler } from "nitro";

// A dynamic route — spans are named by the matched route template
// (`GET /users/:id`), not the concrete path, to keep cardinality low.
export default defineHandler((event) => ({
  user: { id: event.context.params!.id },
}));
```

::





Nitro can instrument its request lifecycle through Node [diagnostics channels](https://nodejs.org/api/diagnostics_channel.html) — no OpenTelemetry SDK required. This example turns instrumentation on and enables the built-in console logger, which groups every h3, srvx and unstorage span into a per-request timeline (waterfall).

## Enabling tracing

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

export default defineConfig({
  // Instrument the h3, srvx and unstorage tracing channels.
  tracingChannel: true,

  experimental: {
    // Log every completed span to the console (built-in, dependency-free sink).
    tracingLogger: true,
  },
});
```

`tracingChannel: true` wires up the producers (it accepts `{ h3, srvx, unstorage }` to trace a subset). `experimental.tracingLogger` adds a built-in sink that `console.log`s each completed span — a dependency-free alternative to a vendor exporter, handy for local development.

## Try it

```sh
npm run dev
# then, in another terminal:
curl http://localhost:3000/
curl http://localhost:3000/users/42
```

Each request prints a timeline of its spans to the console — the middleware, the matched route, and every storage operation, positioned and sized by when they ran and how long they took:

```
▶ GET /  4.10ms  (4 spans)
  middleware GET /             █·······················   0.12ms h3.handler_type=middleware http.route=/
  GET /                        ·███████████████████·····   2.49ms h3.handler_type=route http.route=/
  getItem                      ···██····················   0.18ms db.operation=getItem db.system=memory unstorage.keys_count=1
  setItem                      ·····██··················   0.16ms db.operation=setItem db.system=memory unstorage.keys_count=1
```

The header line (`▶`) is the request itself — its method, path and total time; the rows below are the spans that ran within it. Note the dynamic route is named by its matched template — `GET /users/:id`, not `/users/42` — keeping span names low-cardinality per the OpenTelemetry HTTP conventions.

The request boundary comes from Nitro's `request`/`response` runtime hooks, so grouping works identically in `vite dev` and in a production build. Spans are grouped per request via async context, so timelines for concurrent requests stay separate, and a failed span is marked `✖` with its error message.

## What gets traced

| Channel | Span | Emitted by |
| --- | --- | --- |
| `h3.request` | each matched route and middleware | `server/routes/*`, `server/middleware/*` |
| `unstorage.*` | each storage operation (`getItem`, `setItem`, …) | `useKV()` in `server/routes/index.ts` |
| `srvx.request` | the whole request, with response status (production server) | the srvx server layer |

---

### 4.Examples Virtual Routes (`docs/4.examples/virtual-routes.md`)

---
navigation:
  category: features
icon: i-lucide-box
---

# Virtual Routes

> Define routes programmatically using Nitro's virtual module system.



::code-tree{defaultValue="nitro.config.ts" expandAll}

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

export default defineConfig({
  routes: {
    "/": "#virtual-route",
  },
  virtual: {
    "#virtual-route": () =>
      /* js */ `export default () => new Response("Hello from virtual entry!")`,
  },
});
```

```json [package.json]
{
  "type": "module",
  "scripts": {
    "build": "nitro build",
    "dev": "nitro dev",
    "preview": "node .output/server/index.mjs"
  },
  "devDependencies": {
    "nitro": "latest"
  }
}
```

```json [tsconfig.json]
{
  "extends": "nitro/tsconfig"
}
```

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

export default defineConfig({ plugins: [nitro()] });
```

::





Virtual routes let you define handlers as strings in your config instead of creating separate files. This is useful when generating routes dynamically, building plugins, or keeping simple routes inline.

## Configuration

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

export default defineConfig({
  routes: {
    "/": "#virtual-route",
  },
  virtual: {
    "#virtual-route": () =>
      /* js */ `export default () => new Response("Hello from virtual entry!")`,
  },
});
```

The `routes` option maps URL paths to virtual module identifiers (prefixed with `#`). The `virtual` option defines the module content as a string or function returning a string. At build time, Nitro resolves these virtual modules to actual handlers.

There are no route files in this project. The entire handler is defined inline in the config, and Nitro generates the route at build time.



## Learn More

- [Routing](/docs/routing)
- [Configuration](/docs/configuration)

---

### 4.Examples Vite Nitro Plugin (`docs/4.examples/vite-nitro-plugin.md`)

---
navigation:
  category: vite
icon: i-logos-vitejs
---

# Vite Nitro Plugin

> Use Nitro as a Vite plugin for programmatic configuration.



::code-tree{defaultValue="vite.config.mjs" expandAll}

```json [package.json]
{
  "type": "module",
  "scripts": {
    "build": "vite build",
    "preview": "vite preview",
    "dev": "vite dev"
  },
  "devDependencies": {
    "nitro": "latest",
    "vite": "latest"
  }
}
```

```json [tsconfig.json]
{
  "extends": "nitro/tsconfig"
}
```

```js [vite.config.mjs]
import { defineConfig } from "vite";
import { nitro } from "nitro/vite";

export default defineConfig({
  plugins: [
    nitro(),
    {
      name: "my-nitro-plugin",
      nitro: {
        setup: (nitro) => {
          nitro.options.routes["/"] = "#virtual-by-plugin";
          nitro.options.virtual["#virtual-by-plugin"] =
            `export default () => new Response("Hello from virtual entry!")`;
        },
      },
    },
  ],
});
```

::





Instead of using a separate `nitro.config.ts`, you can configure Nitro directly in your Vite config. This gives you access to Nitro's setup hook where you can register routes and virtual modules programmatically.

## Vite Configuration

```js [vite.config.mjs]
import { defineConfig } from "vite";
import { nitro } from "nitro/vite";

export default defineConfig({
  plugins: [
    nitro(),
    {
      name: "my-nitro-plugin",
      nitro: {
        setup: (nitro) => {
          nitro.options.routes["/"] = "#virtual-by-plugin";
          nitro.options.virtual["#virtual-by-plugin"] =
            `export default () => new Response("Hello from virtual entry!")`;
        },
      },
    },
  ],
});
```

The config adds two plugins: the `nitro()` plugin and a custom plugin that uses the `nitro.setup` hook. Inside the setup function, you have access to Nitro's options object. This example registers a virtual route at `/` that maps to a virtual module `#virtual-by-plugin`, then defines that module inline.



## Learn More

- [Configuration](/docs/configuration)

---

### 4.Examples Vite Rsc (`docs/4.examples/vite-rsc.md`)

---
navigation:
  category: vite
icon: i-logos-react
---

# Vite RSC

> React Server Components with Vite and Nitro.



::code-tree{defaultValue="app/root.tsx" expandAll}

```json [package.json]
{
  "name": "@vitejs/plugin-rsc-examples-starter",
  "version": "0.0.0",
  "private": true,
  "license": "MIT",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "react": "^19.3.1",
    "react-dom": "^19.3.1"
  },
  "devDependencies": {
    "@types/react": "^19.3.0",
    "@types/react-dom": "^19.3.0",
    "@vitejs/plugin-react": "^6.1.2",
    "@vitejs/plugin-rsc": "^0.5.38",
    "nitro": "latest",
    "rsc-html-stream": "^0.0.8",
    "vite": "latest"
  }
}
```

```json [tsconfig.json]
{
  "extends": "nitro/tsconfig",
  "compilerOptions": {
    "lib": ["ESNext", "DOM", "DOM.Iterable"],
    "types": ["vite/client", "@vitejs/plugin-rsc/types"],
    "jsx": "react-jsx"
  }
}
```

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

import rsc from "@vitejs/plugin-rsc";
import react from "@vitejs/plugin-react";

export default defineConfig({
  plugins: [
    nitro(),
    rsc({
      serverHandler: false,
      entries: {
        ssr: "./app/framework/entry.ssr.tsx",
        rsc: "./app/framework/entry.rsc.tsx",
      },
    }),
    react(),
  ],

  environments: {
    client: {
      build: {
        rollupOptions: {
          input: { index: "./app/framework/entry.browser.tsx" },
        },
      },
    },
  },
});
```

```tsx [app/action.tsx]
"use server";

let serverCounter = 0;

export async function getServerCounter() {
  return serverCounter;
}

export async function updateServerCounter(change: number) {
  serverCounter += change;
}
```

```tsx [app/client.tsx]
"use client";

import React from "react";

export function ClientCounter() {
  const [count, setCount] = React.useState(0);

  return <button onClick={() => setCount((count) => count + 1)}>Client Counter: {count}</button>;
}
```

```
/* Detailed source-code truncated for AI context efficiency. */
```

```
/* Detailed source-code truncated for AI context efficiency. */
```

```
/* Detailed source-code truncated for AI context efficiency. */
```

```
/* Detailed source-code truncated for AI context efficiency. */
```

```
/* Detailed source-code truncated for AI context efficiency. */
```

```
/* Detailed source-code truncated for AI context efficiency. */
```

```
/* Detailed source-code truncated for AI context efficiency. */
```

```
/* Detailed source-code truncated for AI context efficiency. */
```

```
/* Detailed source-code truncated for AI context efficiency. */
```

```
/* Detailed source-code truncated for AI context efficiency. */
```

::





This example demonstrates React Server Components (RSC) using Vite's experimental RSC plugin with Nitro. It includes server components, client components, server actions, and streaming SSR.

## Overview

1. **SSR Entry** handles incoming requests and renders React components to HTML
2. **Root Component** defines the page structure as a server component
3. **Client Components** use the `"use client"` directive for interactive parts

## 1. SSR Entry

```
/* Detailed source-code truncated for AI context efficiency. */
```

The SSR entry handles the rendering pipeline. It loads the RSC entry module, duplicates the RSC stream (one for SSR, one for hydration), deserializes the stream back to React VDOM, and renders it to HTML. The RSC payload is injected into the HTML for client hydration.

## 2. Root Server Component

```
/* Detailed source-code truncated for AI context efficiency. */
```

Server components run only on the server. They can import CSS directly, use server-side data, and call server actions. The `ClientCounter` component is imported but runs on the client because it has the `"use client"` directive.

## 3. Client Component

```tsx [app/client.tsx]
"use client";

import React from "react";

export function ClientCounter() {
  const [count, setCount] = React.useState(0);

  return <button onClick={() => setCount((count) => count + 1)}>Client Counter: {count}</button>;
}
```

The `"use client"` directive marks this as a client component. It hydrates on the browser and handles interactive state. Server components can import and render client components, but client components cannot import server components.



## Learn More

- [React Server Components](https://react.dev/reference/rsc/server-components)

---

### 4.Examples Vite Ssr Html (`docs/4.examples/vite-ssr-html.md`)

---
navigation:
  category: server side rendering
icon: i-material-symbols-html
---

# Vite SSR HTML

> Server-side rendering with vanilla HTML, Vite, and Nitro.




::code-tree{defaultValue="app/entry-server.ts" expandAll}

```
/* Detailed source-code truncated for AI context efficiency. */
```

```json [package.json]
{
  "type": "module",
  "scripts": {
    "build": "vite build",
    "dev": "vite dev",
    "preview": "vite preview"
  },
  "devDependencies": {
    "@tailwindcss/vite": "^4.3.3",
    "nitro": "latest",
    "tailwindcss": "^4.3.3",
    "vite": "latest"
  }
}
```

```json [tsconfig.json]
{
  "extends": "nitro/tsconfig"
}
```

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

import tailwindcss from "@tailwindcss/vite";

export default defineConfig({
  plugins: [
    nitro({
      serverDir: "./",
    }),
    tailwindcss(),
  ],
});
```

```ts [app/entry-server.ts]
import { fetch } from "nitro";

export default {
  async fetch() {
    const quote = (await fetch("/quote").then((res) => res.json())) as {
      text: string;
    };
    return tokenizedStream(quote.text, 50);
  },
};

function tokenizedStream(text: string, delay: number): ReadableStream<Uint8Array> {
  const tokens = text.split(" ");
  return new ReadableStream({
    start(controller) {
      let index = 0;
      function push() {
        if (index < tokens.length) {
          const word = tokens[index++] + (index < tokens.length ? " " : "");
          controller.enqueue(new TextEncoder().encode(word));
          setTimeout(push, delay);
        } else {
          controller.close();
        }
      }
      push();
    },
  });
}
```

```ts [routes/quote.ts]
const QUOTES_URL =
  "https://github.com/JamesFT/Database-Quotes-JSON/raw/refs/heads/master/quotes.json";

let _quotes: Promise<unknown> | undefined;

function getQuotes() {
  return (_quotes ??= fetch(QUOTES_URL).then((res) => res.json())) as Promise<
    { quoteText: string; quoteAuthor: string }[]
  >;
}

export default async function quotesHandler() {
  const quotes = await getQuotes();
  const randomQuote = quotes[Math.floor(Math.random() * quotes.length)];
  return Response.json({
    text: randomQuote.quoteText,
    author: randomQuote.quoteAuthor,
  });
}
```

::





This example renders an HTML template with server-side data and streams the response word by word. It demonstrates how to use Nitro's Vite SSR integration without a framework.

## Overview

1. **Add the Nitro Vite plugin** to enable SSR
2. **Create an HTML template** with a `` comment where server content goes
3. **Create a server entry** that fetches data and returns a stream
4. **Add API routes** for server-side data

## How It Works

The `index.html` file contains an `` comment that marks where server-rendered content will be inserted. Nitro replaces this comment with the output from your server entry.

The server entry exports an object with a `fetch` method. It calls the `/quote` API route using Nitro's internal fetch, then returns a `ReadableStream` that emits the quote text word by word with a 50ms delay between each word.

The quote route fetches a JSON file of quotes from GitHub, caches the result, and returns a random quote. The server entry calls this route to get content for the page.



## Learn More

- [Renderer](/docs/renderer)
- [Server Entry](/docs/server-entry)

---

### 4.Examples Vite Ssr Preact (`docs/4.examples/vite-ssr-preact.md`)

---
navigation:
  category: server side rendering
icon: i-logos-preact
---

# SSR with Preact

> Server-side rendering with Preact in Nitro using Vite.



::code-tree{defaultValue="src/entry-server.tsx" expandAll}

```json [package.json]
{
  "type": "module",
  "scripts": {
    "build": "vite build",
    "preview": "vite preview",
    "dev": "vite dev"
  },
  "devDependencies": {
    "@preact/preset-vite": "^2.10.6",
    "@tailwindcss/vite": "^4.3.3",
    "nitro": "latest",
    "preact": "^11.0.1",
    "preact-render-to-string": "^6.8.1",
    "tailwindcss": "^4.3.3",
    "vite": "latest"
  }
}
```

```json [tsconfig.json]
{
  "extends": "nitro/tsconfig",
  "compilerOptions": {
    "jsx": "react-jsx",
    "jsxImportSource": "preact"
  }
}
```

```js [vite.config.mjs]
import { defineConfig } from "vite";
import { nitro } from "nitro/vite";
import preact from "@preact/preset-vite";

export default defineConfig({
  plugins: [nitro(), preact()],
  environments: {
    client: {
      build: {
        rollupOptions: {
          input: "./src/entry-client.tsx",
        },
      },
    },
  },
});
```

```tsx [src/app.tsx]
import { useState } from "preact/hooks";

export function App() {
  const [count, setCount] = useState(0);
  return <button onClick={() => setCount((c) => c + 1)}>Count is {count}</button>;
}
```

```tsx [src/entry-client.tsx]
import { hydrate } from "preact";
import { App } from "./app.tsx";

function main() {
  hydrate(<App />, document.querySelector("#app")!);
}

main();
```

```tsx [src/entry-server.tsx]
import "./styles.css";
import { renderToReadableStream } from "preact-render-to-string/stream";
import { App } from "./app.jsx";

import clientAssets from "./entry-client?assets=client";
import serverAssets from "./entry-server?assets=ssr";

export default {
  async fetch(request: Request) {
    const url = new URL(request.url);
    const htmlStream = renderToReadableStream(<Root url={url} />);
    return new Response(htmlStream, {
      headers: { "Content-Type": "text/html;charset=utf-8" },
    });
  },
};

function Root(props: { url: URL }) {
  const assets = clientAssets.merge(serverAssets);
  return (
    <html lang="en">
      <head>
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        {assets.css.map((attr: any) => (
          <link key={attr.href} rel="stylesheet" {...attr} />
        ))}
        {assets.js.map((attr: any) => (
          <link key={attr.href} rel="modulepreload" {...attr} />
        ))}
        <script type="module" src={assets.entry} />
      </head>
      <body>
        <h1 className="hero">Nitro + Vite + Preact</h1>
        <p>URL: {props.url.href}</p>
        <div id="app">
          <App />
        </div>
      </body>
    </html>
  );
}
```

```css [src/styles.css]
.hero {
  color: orange;
}

button {
  background-color: lightskyblue;
}
```

::





Set up server-side rendering (SSR) with Preact, Vite, and Nitro. This setup enables streaming HTML responses, automatic asset management, and client hydration.

## Overview

1. Add the Nitro Vite plugin to your Vite config
2. Configure client and server entry points
3. Create a server entry that renders your app to HTML
4. Create a client entry that hydrates the server-rendered HTML

## 1. Configure Vite

Add the Nitro and Preact plugins to your Vite config. Define the `client` environment with your client entry point:

```js [vite.config.mjs]
import { defineConfig } from "vite";
import { nitro } from "nitro/vite";
import preact from "@preact/preset-vite";

export default defineConfig({
  plugins: [nitro(), preact()],
  environments: {
    client: {
      build: {
        rollupOptions: {
          input: "./src/entry-client.tsx",
        },
      },
    },
  },
});
```

The `environments.client` configuration tells Vite which file to use as the browser entry point. Nitro automatically detects the SSR entry from a file named `entry-server` in `app/`, `src/`, or the project root.

## 2. Create the App Component

Create a shared Preact component that runs on both server and client:

```tsx [src/app.tsx]
import { useState } from "preact/hooks";

export function App() {
  const [count, setCount] = useState(0);
  return <button onClick={() => setCount((c) => c + 1)}>Count is {count}</button>;
}
```

## 3. Create the Server Entry

The server entry renders your Preact app to a streaming HTML response using `preact-render-to-string/stream`:

```tsx [src/entry-server.tsx]
import "./styles.css";
import { renderToReadableStream } from "preact-render-to-string/stream";
import { App } from "./app.jsx";

import clientAssets from "./entry-client?assets=client";
import serverAssets from "./entry-server?assets=ssr";

export default {
  async fetch(request: Request) {
    const url = new URL(request.url);
    const htmlStream = renderToReadableStream(<Root url={url} />);
    return new Response(htmlStream, {
      headers: { "Content-Type": "text/html;charset=utf-8" },
    });
  },
};

function Root(props: { url: URL }) {
  const assets = clientAssets.merge(serverAssets);
  return (
    <html lang="en">
      <head>
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        {assets.css.map((attr: any) => (
          <link key={attr.href} rel="stylesheet" {...attr} />
        ))}
        {assets.js.map((attr: any) => (
          <link key={attr.href} rel="modulepreload" {...attr} />
        ))}
        <script type="module" src={assets.entry} />
      </head>
      <body>
        <h1 className="hero">Nitro + Vite + Preact</h1>
        <p>URL: {props.url.href}</p>
        <div id="app">
          <App />
        </div>
      </body>
    </html>
  );
}
```

Import assets using the `?assets=client` and `?assets=ssr` query parameters. Nitro collects CSS and JS assets from each entry point, and `merge()` combines them into a single manifest. The `assets` object provides arrays of stylesheet and script attributes, plus the client entry URL. Use `renderToReadableStream` to stream HTML as Preact renders, improving time-to-first-byte.

## 4. Create the Client Entry

The client entry hydrates the server-rendered HTML, attaching Preact's event handlers:

```tsx [src/entry-client.tsx]
import { hydrate } from "preact";
import { App } from "./app.tsx";

function main() {
  hydrate(<App />, document.querySelector("#app")!);
}

main();
```

The `hydrate` function attaches Preact to the existing server-rendered DOM inside `#app` without re-rendering it.



## Learn More

- [Renderer](/docs/renderer)
- [Server Entry](/docs/server-entry)

---

### 4.Examples Vite Ssr React Router (`docs/4.examples/vite-ssr-react-router.md`)

---
navigation:
  category: server side rendering
icon: i-logos-react
---

# SSR with React Router

> Server-side rendering with React Router in Nitro using Vite.



::code-tree{defaultValue="server/ssr.ts" expandAll}

```json [package.json]
{
  "name": "vite-ssr-react-router",
  "private": true,
  "type": "module",
  "scripts": {
    "build": "vite build",
    "dev": "vite dev",
    "start": "node ./build/server/index.mjs",
    "typegen": "react-router typegen"
  },
  "dependencies": {
    "@react-router/node": "^8.5.0",
    "isbot": "^5.2.2",
    "react": "^19.3.1",
    "react-dom": "^19.3.1",
    "react-router": "^8.5.0"
  },
  "devDependencies": {
    "@react-router/dev": "^8.5.0",
    "@tailwindcss/vite": "^4.3.3",
    "@types/node": "^26.6.5",
    "@types/react": "^19.3.0",
    "@types/react-dom": "^19.3.0",
    "nitro": "latest",
    "tailwindcss": "^4.3.3",
    "typescript": "^7.0.2",
    "vite": "latest"
  }
}
```

```ts [react-router.config.ts]
import type { Config } from "@react-router/dev/config";

export default {
  // Config options...
  // Server-side render by default, to enable SPA mode set this to `false`
  ssr: true,
  buildDirectory: "build",
} satisfies Config;
```

```json [tsconfig.json]
{
  "extends": "nitro/tsconfig",
  "include": ["**/*", "**/.server/**/*", "**/.client/**/*", ".react-router/types/**/*"],
  "compilerOptions": {
    "types": ["node", "vite/client"],
    "jsx": "react-jsx",
    "rootDirs": [".", "./.react-router/types"],
    "paths": {
      "~/*": ["./app/*"]
    }
  }
}
```

```ts [vite.config.ts]
import { reactRouter } from "@react-router/dev/vite";
import tailwindcss from "@tailwindcss/vite";
import { defineConfig } from "vite";
import { nitro } from "nitro/vite";

import reactRouterConfig from "./react-router.config";

export default defineConfig({
  plugins: [
    tailwindcss(),
    reactRouter(),
    nitro({
      serverDir: "./server",
      output: {
        dir: reactRouterConfig.buildDirectory,
        serverDir: `${reactRouterConfig.buildDirectory}/server`,
        publicDir: `${reactRouterConfig.buildDirectory}/client`,
      },
    }),
  ],
  resolve: {
    tsconfigPaths: true,
  },
  environments: {
    ssr: { build: { rollupOptions: { input: "./server/ssr.ts" } } },
  },
});
```

```css [app/app.css]
@import "tailwindcss";

@theme {
  --font-sans:
    "Inter", ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji",
    "Segoe UI Symbol", "Noto Color Emoji";
}

html,
body {
  @apply bg-white dark:bg-gray-950;

  @media (prefers-color-scheme: dark) {
    color-scheme: dark;
  }
}
```

```
/* Detailed source-code truncated for AI context efficiency. */
```

```ts [app/routes.ts]
import { type RouteConfig, index } from "@react-router/dev/routes";

export default [index("routes/home.tsx")] satisfies RouteConfig;
```

```ts [server/ssr.ts]
import { createRequestHandler } from "react-router";

export default {
  fetch: createRequestHandler(
    () => import("virtual:react-router/server-build"),
    import.meta.env.MODE
  ),
};
```

```
/* Detailed source-code truncated for AI context efficiency. */
```

```ts [server/routes/health.get.ts]
import { defineHandler } from "nitro";

export default defineHandler(() => {
  return { status: "OK" };
});
```

::





Set up React Router framework mode with Vite and Nitro. This setup uses Nitro to run the React Router server build in development and production.

## Overview

1. Add the React Router and Nitro plugins to your Vite config
2. Configure Nitro to emit its server alongside React Router's client build
3. Create an SSR handler using React Router's request handler
4. Add Nitro server routes
5. Define routes using React Router's route configuration

## 1. Configure Vite

Add the React Router, Tailwind CSS, and Nitro plugins to your Vite config:

```ts [vite.config.ts]
import { reactRouter } from "@react-router/dev/vite";
import tailwindcss from "@tailwindcss/vite";
import { defineConfig } from "vite";
import { nitro } from "nitro/vite";

import reactRouterConfig from "./react-router.config";

export default defineConfig({
  plugins: [
    tailwindcss(),
    reactRouter(),
    nitro({
      serverDir: "./server",
      output: {
        dir: reactRouterConfig.buildDirectory,
        serverDir: `${reactRouterConfig.buildDirectory}/server`,
        publicDir: `${reactRouterConfig.buildDirectory}/client`,
      },
    }),
  ],
  resolve: {
    tsconfigPaths: true,
  },
  environments: {
    ssr: { build: { rollupOptions: { input: "./server/ssr.ts" } } },
  },
});
```

React Router creates the `ssr` environment and builds browser assets into `build/client`. The custom input points to the fetch-compatible handler in `server/ssr.ts`. Nitro scans `server/` for its own routes, then emits the production server into `build/server`.

## 2. Configure React Router

Enable SSR and keep the build directory in sync with the Nitro output configuration:

```ts [react-router.config.ts]
import type { Config } from "@react-router/dev/config";

export default {
  ssr: true,
  buildDirectory: "build",
} satisfies Config;
```

React Router generates the server build exposed by `virtual:react-router/server-build` and the client assets required for hydration.

## 3. Create the SSR Handler

Create an SSR handler that delegates incoming requests to React Router:

```ts [server/ssr.ts]
import { createRequestHandler } from "react-router";

export default {
  fetch: createRequestHandler(
    () => import("virtual:react-router/server-build"),
    import.meta.env.MODE
  ),
};
```

Nitro invokes the exported Web `fetch` handler through the Vite `ssr` service in development and production. The request handler loads React Router's generated server build and renders the matched route.

## 4. Add Nitro Server Routes

Add API and other server routes under `server/routes/`:

```ts [server/routes/health.get.ts]
import { defineHandler } from "nitro";

export default defineHandler(() => {
  return { status: "OK" };
});
```

This route is available at `/health` and is handled by Nitro before requests fall through to the React Router SSR service.

## 5. Define React Router Routes

Declare the application's route modules in `app/routes.ts`:

```ts [app/routes.ts]
import { type RouteConfig, index } from "@react-router/dev/routes";

export default [index("routes/home.tsx")] satisfies RouteConfig;
```

The index route renders the home page at `/`.



## Learn More

- [React Router Documentation](https://reactrouter.com/)
- [Directory options](/docs/configuration#directory-options)

---

### 4.Examples Vite Ssr React (`docs/4.examples/vite-ssr-react.md`)

---
navigation:
  category: server side rendering
icon: i-logos-react
---

# SSR with React

> Server-side rendering with React in Nitro using Vite.



::code-tree{defaultValue="src/entry-server.tsx" expandAll}

```json [package.json]
{
  "type": "module",
  "scripts": {
    "build": "vite build",
    "preview": "vite preview",
    "dev": "vite dev"
  },
  "devDependencies": {
    "@types/react": "^19.3.0",
    "@types/react-dom": "^19.3.0",
    "@vitejs/plugin-react": "^6.1.2",
    "nitro": "latest",
    "react": "^19.3.1",
    "react-dom": "^19.3.1",
    "react-refresh": "^0.19.0",
    "vite": "latest"
  }
}
```

```json [tsconfig.json]
{
  "extends": "nitro/tsconfig",
  "compilerOptions": {
    "jsx": "react-jsx",
    "jsxImportSource": "react"
  }
}
```

```js [vite.config.mjs]
import { defineConfig } from "vite";
import { nitro } from "nitro/vite";
import react from "@vitejs/plugin-react";

export default defineConfig({
  plugins: [nitro(), react()],
  environments: {
    client: {
      build: { rollupOptions: { input: "./src/entry-client.tsx" } },
    },
  },
});
```

```tsx [src/app.tsx]
import { useState } from "react";

export function App() {
  const [count, setCount] = useState(0);
  return (
    <>
      <h1 className="hero">Nitro + Vite + React</h1>
      <button onClick={() => setCount((c) => c + 1)}>Count is {count}</button>
    </>
  );
}
```

```tsx [src/entry-client.tsx]
import "@vitejs/plugin-react/preamble";
import { hydrateRoot } from "react-dom/client";
import { App } from "./app.tsx";

hydrateRoot(document.querySelector("#app")!, <App />);
```

```tsx [src/entry-server.tsx]
import "./styles.css";
import { renderToReadableStream } from "react-dom/server.edge";
import { App } from "./app.tsx";

import clientAssets from "./entry-client?assets=client";
import serverAssets from "./entry-server?assets=ssr";

export default {
  async fetch(_req: Request) {
    const assets = clientAssets.merge(serverAssets);
    return new Response(
      await renderToReadableStream(
        <html lang="en">
          <head>
            <meta name="viewport" content="width=device-width, initial-scale=1.0" />
            {assets.css.map((attr: any) => (
              <link key={attr.href} rel="stylesheet" {...attr} />
            ))}
            {assets.js.map((attr: any) => (
              <link key={attr.href} rel="modulepreload" {...attr} />
            ))}
            <script type="module" src={assets.entry} />
          </head>
          <body id="app">
            <App />
          </body>
        </html>
      ),
      { headers: { "Content-Type": "text/html;charset=utf-8" } }
    );
  },
};
```

```css [src/styles.css]
.hero {
  color: orange;
}

button {
  background-color: lightskyblue;
}
```

::





Set up server-side rendering (SSR) with React, Vite, and Nitro. This setup enables streaming HTML responses, automatic asset management, and client hydration.

## Overview

1. Add the Nitro Vite plugin to your Vite config
2. Configure client and server entry points
3. Create a server entry that renders your app to HTML
4. Create a client entry that hydrates the server-rendered HTML

## 1. Configure Vite

Add the Nitro and React plugins to your Vite config. Define the `client` environment with your client entry point:

```js [vite.config.mjs]
import { defineConfig } from "vite";
import { nitro } from "nitro/vite";
import react from "@vitejs/plugin-react";

export default defineConfig({
  plugins: [nitro(), react()],
  environments: {
    client: {
      build: { rollupOptions: { input: "./src/entry-client.tsx" } },
    },
  },
});
```

The `environments.client` configuration tells Vite which file to use as the browser entry point. Nitro automatically detects the SSR entry from a file named `entry-server` in `app/`, `src/`, or the project root.

## 2. Create the App Component

Create a shared React component that runs on both server and client:

```tsx [src/app.tsx]
import { useState } from "react";

export function App() {
  const [count, setCount] = useState(0);
  return (
    <>
      <h1 className="hero">Nitro + Vite + React</h1>
      <button onClick={() => setCount((c) => c + 1)}>Count is {count}</button>
    </>
  );
}
```

## 3. Create the Server Entry

The server entry renders your React app to a streaming HTML response. It uses `react-dom/server.edge` for edge-compatible streaming:

```tsx [src/entry-server.tsx]
import "./styles.css";
import { renderToReadableStream } from "react-dom/server.edge";
import { App } from "./app.tsx";

import clientAssets from "./entry-client?assets=client";
import serverAssets from "./entry-server?assets=ssr";

export default {
  async fetch(_req: Request) {
    const assets = clientAssets.merge(serverAssets);
    return new Response(
      await renderToReadableStream(
        <html lang="en">
          <head>
            <meta name="viewport" content="width=device-width, initial-scale=1.0" />
            {assets.css.map((attr: any) => (
              <link key={attr.href} rel="stylesheet" {...attr} />
            ))}
            {assets.js.map((attr: any) => (
              <link key={attr.href} rel="modulepreload" {...attr} />
            ))}
            <script type="module" src={assets.entry} />
          </head>
          <body id="app">
            <App />
          </body>
        </html>
      ),
      { headers: { "Content-Type": "text/html;charset=utf-8" } }
    );
  },
};
```

Import assets using the `?assets=client` and `?assets=ssr` query parameters. Nitro collects CSS and JS assets from each entry point, and `merge()` combines them into a single manifest. The `assets` object provides arrays of stylesheet and script attributes, plus the client entry URL. Use `renderToReadableStream` to stream HTML as React renders, improving time-to-first-byte.

## 4. Create the Client Entry

The client entry hydrates the server-rendered HTML, attaching React's event handlers:

```tsx [src/entry-client.tsx]
import "@vitejs/plugin-react/preamble";
import { hydrateRoot } from "react-dom/client";
import { App } from "./app.tsx";

hydrateRoot(document.querySelector("#app")!, <App />);
```

The `@vitejs/plugin-react/preamble` import is required for React Fast Refresh during development. The `hydrateRoot` function attaches React to the existing server-rendered DOM without re-rendering it.



## Learn More

- [Renderer](/docs/renderer)
- [Server Entry](/docs/server-entry)

---

### 4.Examples Vite Ssr Solid (`docs/4.examples/vite-ssr-solid.md`)

---
navigation:
  category: server side rendering
icon: i-logos-solidjs-icon
---

# SSR with SolidJS

> Server-side rendering with SolidJS in Nitro using Vite.



::code-tree{defaultValue="src/entry-server.tsx" expandAll}

```json [package.json]
{
  "type": "module",
  "scripts": {
    "build": "vite build",
    "dev": "vite dev"
  },
  "devDependencies": {
    "nitro": "latest",
    "solid-js": "^1.9.17",
    "vite": "latest",
    "vite-plugin-solid": "^2.11.14"
  }
}
```

```json [tsconfig.json]
{
  "extends": "nitro/tsconfig",
  "compilerOptions": {
    "jsx": "preserve",
    "jsxImportSource": "solid-js"
  }
}
```

```js [vite.config.mjs]
import solid from "vite-plugin-solid";
import { defineConfig } from "vite";
import { nitro } from "nitro/vite";

export default defineConfig({
  plugins: [solid({ ssr: true }), nitro()],
  esbuild: { jsx: "preserve", jsxImportSource: "solid-js" },
  environments: {
    ssr: {
      build: { rollupOptions: { input: "./src/entry-server.tsx" } },
    },
    client: {
      build: { rollupOptions: { input: "./src/entry-client.tsx" } },
    },
  },
});
```

```tsx [src/app.tsx]
import { createSignal } from "solid-js";

export function App() {
  const [count, setCount] = createSignal(0);

  return (
    <div>
      <h1>Hello, Solid!</h1>
      <button onClick={() => setCount((count) => count + 1)}>Count: {count()}</button>
    </div>
  );
}
```

```tsx [src/entry-client.tsx]
import { hydrate } from "solid-js/web";
import "./styles.css";
import { App } from "./app.jsx";

hydrate(() => <App />, document.querySelector("#app")!);
```

```tsx [src/entry-server.tsx]
import { renderToStringAsync, HydrationScript } from "solid-js/web";
import { App } from "./app.jsx";

import clientAssets from "./entry-client?assets=client";
import serverAssets from "./entry-server?assets=ssr";

export default {
  async fetch(req: Request): Promise<Response> {
    const appHTML = await renderToStringAsync(() => <App />);
    const rootHTML = await renderToStringAsync(() => <Root appHTML={appHTML} />);
    return new Response(rootHTML, {
      headers: { "Content-Type": "text/html" },
    });
  },
};

function Root(props: { appHTML?: string }) {
  const assets = clientAssets.merge(serverAssets);
  return (
    <html lang="en">
      <head>
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        {assets.css.map((attr: any) => (
          <link key={attr.href} rel="stylesheet" {...attr} />
        ))}
        {assets.js.map((attr: any) => (
          <link key={attr.href} rel="modulepreload" {...attr} />
        ))}
      </head>
      <body>
        <div id="app" innerHTML={props.appHTML || ""} />
        <HydrationScript />
        <script type="module" src={assets.entry} />
      </body>
    </html>
  );
}
```

```css [src/styles.css]
div {
  font-family: system-ui, Arial, sans-serif;
  font-size: 20px;
  margin-bottom: 10px;
}

button {
  background-color: rgb(147 197 253);
  color: rgb(15 23 42);
  border: none;
  padding: 10px 20px;
  font-size: 16px;
  cursor: pointer;
  border-radius: 5px;
}

button:hover {
  background-color: rgb(191 219 254);
}
```

::





Set up server-side rendering (SSR) with SolidJS, Vite, and Nitro. This setup uses `renderToStringAsync` for HTML generation and supports client hydration.

## Overview

1. Add the Nitro Vite plugin to your Vite config
2. Configure client and server entry points
3. Create a server entry that renders your app to HTML
4. Create a client entry that hydrates the server-rendered HTML

## 1. Configure Vite

Add the Nitro and SolidJS plugins to your Vite config. SolidJS requires explicit JSX configuration and both `ssr` and `client` environments:

```js [vite.config.mjs]
import solid from "vite-plugin-solid";
import { defineConfig } from "vite";
import { nitro } from "nitro/vite";

export default defineConfig({
  plugins: [solid({ ssr: true }), nitro()],
  esbuild: { jsx: "preserve", jsxImportSource: "solid-js" },
  environments: {
    ssr: {
      build: { rollupOptions: { input: "./src/entry-server.tsx" } },
    },
    client: {
      build: { rollupOptions: { input: "./src/entry-client.tsx" } },
    },
  },
});
```

Enable SSR mode in the Solid plugin with `solid({ ssr: true })`. Configure esbuild to preserve JSX for Solid's compiler and use Solid's JSX runtime. SolidJS requires explicit `ssr` and `client` environment configuration in Vite.

## 2. Create the App Component

Create a shared SolidJS component using reactive signals:

```tsx [src/app.tsx]
import { createSignal } from "solid-js";

export function App() {
  const [count, setCount] = createSignal(0);

  return (
    <div>
      <h1>Hello, Solid!</h1>
      <button onClick={() => setCount((count) => count + 1)}>Count: {count()}</button>
    </div>
  );
}
```

SolidJS uses signals (`createSignal`) for state management. Unlike React's `useState`, signals are getter functions that you call to read the value.

## 3. Create the Server Entry

The server entry renders your SolidJS app to HTML using `renderToStringAsync` and includes the `HydrationScript` for client-side hydration:

```tsx [src/entry-server.tsx]
import { renderToStringAsync, HydrationScript } from "solid-js/web";
import { App } from "./app.jsx";

import clientAssets from "./entry-client?assets=client";
import serverAssets from "./entry-server?assets=ssr";

export default {
  async fetch(req: Request): Promise<Response> {
    const appHTML = await renderToStringAsync(() => <App />);
    const rootHTML = await renderToStringAsync(() => <Root appHTML={appHTML} />);
    return new Response(rootHTML, {
      headers: { "Content-Type": "text/html" },
    });
  },
};

function Root(props: { appHTML?: string }) {
  const assets = clientAssets.merge(serverAssets);
  return (
    <html lang="en">
      <head>
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        {assets.css.map((attr: any) => (
          <link key={attr.href} rel="stylesheet" {...attr} />
        ))}
        {assets.js.map((attr: any) => (
          <link key={attr.href} rel="modulepreload" {...attr} />
        ))}
      </head>
      <body>
        <div id="app" innerHTML={props.appHTML || ""} />
        <HydrationScript />
        <script type="module" src={assets.entry} />
      </body>
    </html>
  );
}
```

SolidJS requires rendering the app separately from the shell (two-phase rendering). The app HTML is injected via `innerHTML` to preserve hydration markers. Include the `HydrationScript` component to inject the script Solid needs to rehydrate on the client. Import assets using the `?assets=client` and `?assets=ssr` query parameters to collect CSS and JS from each entry point.

## 4. Create the Client Entry

The client entry hydrates the server-rendered HTML, restoring Solid's reactivity:

```tsx [src/entry-client.tsx]
import { hydrate } from "solid-js/web";
import "./styles.css";
import { App } from "./app.jsx";

hydrate(() => <App />, document.querySelector("#app")!);
```

The `hydrate` function attaches Solid's reactive system to the existing server-rendered DOM inside `#app`. The component is wrapped in a function `() => <App />` as required by Solid's API.



## Learn More

- [SolidJS Documentation](https://docs.solidjs.com/)
- [Renderer](/docs/renderer)
- [Server Entry](/docs/server-entry)

---

### 4.Examples Vite Ssr Solidstart (`docs/4.examples/vite-ssr-solidstart.md`)

---
navigation:
  category: server side rendering
icon: i-logos-solidjs-icon
---

# SSR with SolidStart

> Server-side rendering with SolidStart in Nitro using Vite.



::code-tree{defaultValue="src/entry-server.tsx" expandAll}

```json [package.json]
{
  "private": true,
  "type": "module",
  "scripts": {
    "dev": "vite dev",
    "build": "vite build"
  },
  "dependencies": {
    "@solidjs/meta": "^0.29.4",
    "@solidjs/router": "^1.0.0",
    "@solidjs/start": "^2.0.6",
    "nitro": "latest",
    "solid-js": "^1.9.17",
    "vite": "latest"
  },
  "engines": {
    "node": ">=22"
  }
}
```

```json [tsconfig.json]
{
  "compilerOptions": {
    "target": "ESNext",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "allowSyntheticDefaultImports": true,
    "esModuleInterop": true,
    "jsx": "preserve",
    "jsxImportSource": "solid-js",
    "allowJs": true,
    "strict": true,
    "noEmit": true,
    "isolatedModules": true,
    "types": ["@solidjs/start/env"],
    "paths": {
      "~/*": ["./src/*"]
    }
  }
}
```

```ts [vite.config.ts]
import { defineConfig } from "vite";
import { solidStart } from "@solidjs/start/config";
import { nitro } from "nitro/vite";

export default defineConfig({
  plugins: [solidStart(), nitro()],
});
```

```css [src/app.css]
body {
  font-family:
    Gordita, Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
}

a {
  margin-right: 1rem;
}

main {
  text-align: center;
  padding: 1em;
  margin: 0 auto;
}

h1 {
  color: #335d92;
  text-transform: uppercase;
  font-size: 4rem;
  font-weight: 100;
  line-height: 1.1;
  margin: 4rem auto;
  max-width: 14rem;
}

p {
  max-width: 14rem;
  margin: 2rem auto;
  line-height: 1.35;
}

@media (min-width: 480px) {
  h1 {
    max-width: none;
  }

  p {
    max-width: none;
  }
}
```

```tsx [src/app.tsx]
import { MetaProvider, Title } from "@solidjs/meta";
import { Router } from "@solidjs/router";
import { FileRoutes } from "@solidjs/start/router";
import { Suspense } from "solid-js";
import "./app.css";

export default function App() {
  return (
    <Router
      root={(props) => (
        <MetaProvider>
          <Title>SolidStart - Basic</Title>
          <Suspense>{props.children}</Suspense>
        </MetaProvider>
      )}
    >
      <FileRoutes />
    </Router>
  );
}
```

```tsx [src/entry-client.tsx]
// @refresh reload
import { mount, StartClient } from "@solidjs/start/client";

mount(() => <StartClient />, document.getElementById("app")!);
```

```tsx [src/entry-server.tsx]
// @refresh reload
import { createHandler, StartServer } from "@solidjs/start/server";

export default createHandler(() => (
  <StartServer
    document={({ assets, children, scripts }) => (
      <html lang="en">
        <head>
          <meta charset="utf-8" />
          <meta name="viewport" content="width=device-width, initial-scale=1" />
          {assets}
        </head>
        <body>
          <div id="app">{children}</div>
          {scripts}
        </body>
      </html>
    )}
  />
));
```

```tsx [src/routes/[...404].tsx]
import { Title } from "@solidjs/meta";
import { HttpStatusCode } from "@solidjs/start";

export default function NotFound() {
  return (
    <main>
      <Title>Not Found</Title>
      <HttpStatusCode code={404} />
      <h1>Page Not Found</h1>
      <p>
        Visit{" "}
        <a href="https://start.solidjs.com" target="_blank">
          start.solidjs.com
        </a>{" "}
        to learn how to build SolidStart apps.
      </p>
    </main>
  );
}
```

```tsx [src/routes/index.tsx]
import { Title } from "@solidjs/meta";

export default function Home() {
  return (
    <main>
      <Title>Hello World</Title>
      <h1>Hello world!</h1>
      <p>
        Visit{" "}
        <a href="https://start.solidjs.com" target="_blank">
          start.solidjs.com
        </a>{" "}
        to learn how to build SolidStart apps.
      </p>
    </main>
  );
}
```

::





Set up server-side rendering (SSR) with Solid, Vite, and Nitro. This setup enables streaming HTML responses, automatic asset management, and client hydration.

## Overview

1. Add the Nitro Vite plugin to your Vite config
2. Create a server entry that renders your app to HTML
3. Create a client entry that hydrates the server-rendered HTML

## 1. Configure Vite

Add the SolidStart and Nitro plugins to your Vite config.

```js [vite.config.ts]
import { defineConfig } from "vite";
import { solidStart } from "@solidjs/start/config";
import { nitro } from "nitro/vite";

export default defineConfig({
  plugins: [solidStart(), nitro()],
});
```

## 2. Create the App Component

Create a shared Solid component that runs on both server and client:

```tsx [src/app.tsx]
import { MetaProvider, Title } from "@solidjs/meta";
import { Router } from "@solidjs/router";
import { FileRoutes } from "@solidjs/start/router";
import { Suspense } from "solid-js";

export default function App() {
  return (
    <Router
      root={(props) => (
        <MetaProvider>
          <Title>SolidStart - Basic</Title>
          <Suspense>{props.children}</Suspense>
        </MetaProvider>
      )}
    >
      <FileRoutes />
    </Router>
  );
}
```

## 3. Create the Server Entry

The server entry renders your Solid app to a streaming HTML response:

```tsx [src/entry-server.tsx]
// @refresh reload
import { createHandler, StartServer } from "@solidjs/start/server";

export default createHandler(() => (
  <StartServer
    document={({ assets, children, scripts }) => (
      <html lang="en">
        <head>
          <meta charset="utf-8" />
          <meta name="viewport" content="width=device-width, initial-scale=1" />
          {assets}
        </head>
        <body>
          <div id="app">{children}</div>
          {scripts}
        </body>
      </html>
    )}
  />
));
```

## 4. Create the Client Entry

The client entry hydrates the server-rendered HTML, attaching Solid's event handlers:

```tsx [src/entry-client.tsx]
// @refresh reload
import { mount, StartClient } from "@solidjs/start/client";

mount(() => <StartClient />, document.getElementById("app")!);
```



## Learn More

- [SolidJS Documentation](https://docs.solidjs.com/)
- [Renderer](/docs/renderer)
- [Server Entry](/docs/server-entry)

---

### 4.Examples Vite Ssr Tsr React (`docs/4.examples/vite-ssr-tsr-react.md`)

---
navigation:
  category: server side rendering
icon: i-simple-icons-tanstack
---

# SSR with TanStack Router

> Client-side routing with TanStack Router in Nitro using Vite.



::code-tree{defaultValue="src/main.tsx" expandAll}

```html [index.html]
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Nitro + TanStack Router + React</title>
  </head>

  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>
```

```json [package.json]
{
  "type": "module",
  "scripts": {
    "build": "vite build",
    "dev": "vite dev",
    "preview": "vite preview"
  },
  "devDependencies": {
    "@tanstack/react-router": "^1.170.45",
    "@tanstack/react-router-devtools": "^1.167.2",
    "@tanstack/router-plugin": "^1.168.47",
    "@types/react": "^19.3.0",
    "@types/react-dom": "^19.3.0",
    "@vitejs/plugin-react": "^6.1.2",
    "nitro": "latest",
    "react": "^19.3.1",
    "react-dom": "^19.3.1",
    "vite": "latest"
  }
}
```

```json [tsconfig.json]
{
  "extends": "nitro/tsconfig",
  "compilerOptions": {
    "baseUrl": ".",
    "jsx": "react-jsx",
    "paths": {
      "@/*": ["./src/*"]
    }
  }
}
```

```js [vite.config.mjs]
import { defineConfig } from "vite";
import { nitro } from "nitro/vite";
import react from "@vitejs/plugin-react";
import { tanstackRouter } from "@tanstack/router-plugin/vite";

export default defineConfig({
  plugins: [tanstackRouter({ target: "react", autoCodeSplitting: true }), react(), nitro()],
});
```

```tsx [src/main.tsx]
import { StrictMode } from "react";
import ReactDOM from "react-dom/client";
import { RouterProvider, createRouter } from "@tanstack/react-router";

// Import the generated route tree
import { routeTree } from "./routeTree.gen.ts";

// Create a new router instance
const router = createRouter({ routeTree });

// Register the router instance for type safety
declare module "@tanstack/react-router" {
  interface Register {
    router: typeof router;
  }
}

// Render the app
const rootElement = document.querySelector("#root")!;
if (!rootElement.innerHTML) {
  const root = ReactDOM.createRoot(rootElement);
  root.render(
    <StrictMode>
      <RouterProvider router={router} />
    </StrictMode>
  );
}
```

```
/* Detailed source-code truncated for AI context efficiency. */
```

```
/* Detailed source-code truncated for AI context efficiency. */
```

```tsx [src/routes/__root.tsx]
import { createRootRoute, Link, Outlet } from "@tanstack/react-router";
import { TanStackRouterDevtools } from "@tanstack/react-router-devtools";

const RootLayout = () => (
  <>
    <div className="p-2 flex gap-2">
      <Link to="/" className="[&.active]:font-bold">
        Home
      </Link>
    </div>
    <hr />
    <Outlet />
    <TanStackRouterDevtools />
  </>
);

export const Route = createRootRoute({ component: RootLayout });
```

```tsx [src/routes/index.tsx]
import { createFileRoute } from "@tanstack/react-router";

export const Route = createFileRoute("/")({
  loader: async () => {
    const r = await fetch("/api/hello");
    return r.json();
  },
  component: Index,
});

function Index() {
  const r = Route.useLoaderData();

  return (
    <div className="p-2">
      <h3>{JSON.stringify(r)}</h3>
    </div>
  );
}
```

::





Set up TanStack Router with React, Vite, and Nitro. This setup provides file-based routing with type-safe navigation and automatic code splitting.

## Overview

1. Add the Nitro Vite plugin to your Vite config
2. Create an HTML template with your app entry
3. Create a main entry that initializes the router
4. Define routes using file-based routing

## 1. Configure Vite

Add the Nitro, React, and TanStack Router plugins to your Vite config:

```js [vite.config.mjs]
import { defineConfig } from "vite";
import { nitro } from "nitro/vite";
import react from "@vitejs/plugin-react";
import { tanstackRouter } from "@tanstack/router-plugin/vite";

export default defineConfig({
  plugins: [tanstackRouter({ target: "react", autoCodeSplitting: true }), react(), nitro()],
});
```

The `tanstackRouter` plugin generates a route tree from your `routes/` directory structure. Enable `autoCodeSplitting` to automatically split routes into separate chunks. Place the TanStack Router plugin before the React plugin in the array.

## 2. Create the HTML Template

Create an HTML file that serves as your app shell:

```html [index.html]
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Nitro + TanStack Router + React</title>
  </head>

  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>
```

## 3. Create the App Entry

Create the main entry that initializes TanStack Router:

```tsx [src/main.tsx]
import { StrictMode } from "react";
import ReactDOM from "react-dom/client";
import { RouterProvider, createRouter } from "@tanstack/react-router";

// Import the generated route tree
import { routeTree } from "./routeTree.gen.ts";

// Create a new router instance
const router = createRouter({ routeTree });

// Register the router instance for type safety
declare module "@tanstack/react-router" {
  interface Register {
    router: typeof router;
  }
}

// Render the app
const rootElement = document.querySelector("#root")!;
if (!rootElement.innerHTML) {
  const root = ReactDOM.createRoot(rootElement);
  root.render(
    <StrictMode>
      <RouterProvider router={router} />
    </StrictMode>
  );
}
```

The `routeTree.gen.ts` file is auto-generated from your `routes/` directory structure. The `Register` interface declaration provides full type inference for route paths and params. The `!rootElement.innerHTML` check prevents re-rendering during hot module replacement.

## 4. Create the Root Route

The root route (`__root.tsx`) defines your app's layout:

```tsx [src/routes/__root.tsx]
import { createRootRoute, Link, Outlet } from "@tanstack/react-router";
import { TanStackRouterDevtools } from "@tanstack/react-router-devtools";

const RootLayout = () => (
  <>
    <div className="p-2 flex gap-2">
      <Link to="/" className="[&.active]:font-bold">
        Home
      </Link>
    </div>
    <hr />
    <Outlet />
    <TanStackRouterDevtools />
  </>
);

export const Route = createRootRoute({ component: RootLayout });
```

Use `Link` for type-safe navigation with active state styling. The `Outlet` component renders child routes. Include `TanStackRouterDevtools` for development tools (automatically removed in production).

## 5. Create Page Routes

Page routes use `createFileRoute` and can include loaders:

```tsx [src/routes/index.tsx]
import { createFileRoute } from "@tanstack/react-router";

export const Route = createFileRoute("/")({
  loader: async () => {
    const r = await fetch("/api/hello");
    return r.json();
  },
  component: Index,
});

function Index() {
  const r = Route.useLoaderData();

  return (
    <div className="p-2">
      <h3>{JSON.stringify(r)}</h3>
    </div>
  );
}
```

Fetch data before rendering with the `loader` function—data is available via `Route.useLoaderData()`. File paths determine URL paths: `routes/index.tsx` maps to `/`, `routes/about.tsx` to `/about`, and `routes/users/$id.tsx` to `/users/:id`.



## Learn More

- [TanStack Router Documentation](https://tanstack.com/router)
- [Renderer](/docs/renderer)

---

### 4.Examples Vite Ssr Tss React (`docs/4.examples/vite-ssr-tss-react.md`)

---
navigation:
  category: server side rendering
icon: i-simple-icons-tanstack
---

# SSR with TanStack Start

> Full-stack React with TanStack Start in Nitro using Vite.



::code-tree{defaultValue="server.ts" expandAll}

```json [package.json]
{
  "type": "module",
  "scripts": {
    "build": "vite build",
    "dev": "vite dev",
    "start": "node .output/server/index.mjs"
  },
  "dependencies": {
    "@tanstack/react-router": "^1.170.45",
    "@tanstack/react-router-devtools": "^1.167.2",
    "@tanstack/react-start": "^1.169.3",
    "nitro": "latest",
    "react": "^19.3.1",
    "react-dom": "^19.3.1",
    "tailwind-merge": "^3.7.0",
    "zod": "^4.6.7"
  },
  "devDependencies": {
    "@tailwindcss/vite": "^4.3.3",
    "@types/node": "latest",
    "@types/react": "^19.3.0",
    "@types/react-dom": "^19.3.0",
    "@vitejs/plugin-react": "^6.1.2",
    "tailwindcss": "^4.3.3",
    "typescript": "^7.0.2",
    "vite": "latest"
  }
}
```

```ts [server.ts]
import handler, { createServerEntry } from "@tanstack/react-start/server-entry";

export default createServerEntry({
  fetch(request) {
    return handler.fetch(request);
  },
});
```

```json [tsconfig.json]
{
  "extends": "nitro/tsconfig",
  "compilerOptions": {
    "baseUrl": ".",
    "jsx": "react-jsx",
    "paths": {
      "~/*": ["./src/*"]
    }
  }
}
```

```js [vite.config.mjs]
import { defineConfig } from "vite";
import { nitro } from "nitro/vite";
import { tanstackStart } from "@tanstack/react-start/plugin/vite";
import viteReact from "@vitejs/plugin-react";
import tailwindcss from "@tailwindcss/vite";

export default defineConfig({
  plugins: [tanstackStart(), viteReact(), tailwindcss(), nitro()],
  resolve: { tsconfigPaths: true },
  environments: {
    ssr: { build: { rollupOptions: { input: "./server.ts" } } },
  },
});
```

```tsx [src/router.tsx]
import { createRouter } from "@tanstack/react-router";
import { routeTree } from "./routeTree.gen.ts";

export function getRouter() {
  const router = createRouter({
    routeTree,
    defaultPreload: "intent",
    defaultErrorComponent: () => <div>Internal Server Error</div>,
    defaultNotFoundComponent: () => <div>Not Found</div>,
    scrollRestoration: true,
  });
  return router;
}
```

```
/* Detailed source-code truncated for AI context efficiency. */
```

```tsx [src/routes/__root.tsx]
/// <reference types="vite/client" />
import { HeadContent, Link, Scripts, createRootRoute } from "@tanstack/react-router";
import { TanStackRouterDevtools } from "@tanstack/react-router-devtools";
import * as React from "react";
import appCss from "~/styles/app.css?url";

export const Route = createRootRoute({
  head: () => ({
    meta: [
      { charSet: "utf8" },
      { name: "viewport", content: "width=device-width, initial-scale=1" },
    ],
    links: [{ rel: "stylesheet", href: appCss }],
    scripts: [{ src: "/customScript.js", type: "text/javascript" }],
  }),
  errorComponent: () => <h1>500: Internal Server Error</h1>,
  notFoundComponent: () => <h1>404: Page Not Found</h1>,
  shellComponent: RootDocument,
});

function RootDocument({ children }: { children: React.ReactNode }) {
  return (
    <html>
      <head>
        <HeadContent />
      </head>
      <body>
        <div className="p-2 flex gap-2 text-lg">
          <Link to="/" activeProps={{ className: "font-bold" }} activeOptions={{ exact: true }}>
            Home
          </Link>{" "}
          <Link
            // @ts-ignore
            to="/this-route-does-not-exist"
            activeProps={{ className: "font-bold" }}
          >
            404
          </Link>
        </div>
        <hr />
        {children}
        <TanStackRouterDevtools position="bottom-right" />
        <Scripts />
      </body>
    </html>
  );
}
```

```tsx [src/routes/index.tsx]
import { createFileRoute } from "@tanstack/react-router";

export const Route = createFileRoute("/")({ component: Home });

function Home() {
  return (
    <div className="p-2">
      <h3>Welcome Home!</h3>
      <a href="/api/test">/api/test</a>
    </div>
  );
}
```

```css [src/styles/app.css]
@import "tailwindcss";

@layer base {
  *,
  ::after,
  ::before,
  ::backdrop,
  ::file-selector-button {
    border-color: var(--color-gray-200, currentcolor);
  }
}

@layer base {
  html {
    color-scheme: light dark;
  }

  * {
    @apply border-gray-200 dark:border-gray-800;
  }

  html,
  body {
    @apply text-gray-900 bg-gray-50 dark:bg-gray-950 dark:text-gray-200;
  }

  .using-mouse * {
    outline: none !important;
  }
}
```

::





Set up TanStack Start with Nitro for a full-stack React framework experience with server-side rendering, file-based routing, and integrated API routes.

## Overview

1. Add the Nitro Vite plugin to your Vite config
2. Create a server entry using TanStack Start's server handler
3. Configure the router with default components
4. Define routes and API endpoints using file-based routing

## 1. Configure Vite

Add the Nitro, React, TanStack Start, and Tailwind plugins to your Vite config:

```js [vite.config.mjs]
import { defineConfig } from "vite";
import { nitro } from "nitro/vite";
import { tanstackStart } from "@tanstack/react-start/plugin/vite";
import viteReact from "@vitejs/plugin-react";
import tailwindcss from "@tailwindcss/vite";

export default defineConfig({
  plugins: [tanstackStart(), viteReact(), tailwindcss(), nitro()],
  resolve: { tsconfigPaths: true },
  environments: {
    ssr: { build: { rollupOptions: { input: "./server.ts" } } },
  },
});
```

The `tanstackStart()` plugin provides full SSR integration with automatic client entry handling. The `environments.ssr` option points to the server entry file.

## 2. Create the Server Entry

Create a server entry that uses TanStack Start's handler:

```ts [server.ts]
import handler, { createServerEntry } from "@tanstack/react-start/server-entry";

export default createServerEntry({
  fetch(request) {
    return handler.fetch(request);
  },
});
```

TanStack Start handles SSR automatically. The `createServerEntry` wrapper integrates with Nitro's server entry format, and the `handler.fetch` processes all incoming requests.

## 3. Configure the Router

Create a router factory function with default error and not-found components:

```tsx [src/router.tsx]
import { createRouter } from "@tanstack/react-router";
import { routeTree } from "./routeTree.gen.ts";

export function getRouter() {
  const router = createRouter({
    routeTree,
    defaultPreload: "intent",
    defaultErrorComponent: () => <div>Internal Server Error</div>,
    defaultNotFoundComponent: () => <div>Not Found</div>,
    scrollRestoration: true,
  });
  return router;
}
```

The router factory configures preloading behavior, scroll restoration, and default error/not-found components.

## 4. Create the Root Route

The root route defines your HTML shell with head management and scripts:

```tsx [src/routes/__root.tsx]
/// <reference types="vite/client" />
import { HeadContent, Link, Scripts, createRootRoute } from "@tanstack/react-router";
import { TanStackRouterDevtools } from "@tanstack/react-router-devtools";
import * as React from "react";
import appCss from "~/styles/app.css?url";

export const Route = createRootRoute({
  head: () => ({
    meta: [
      { charSet: "utf8" },
      { name: "viewport", content: "width=device-width, initial-scale=1" },
    ],
    links: [{ rel: "stylesheet", href: appCss }],
    scripts: [{ src: "/customScript.js", type: "text/javascript" }],
  }),
  errorComponent: () => <h1>500: Internal Server Error</h1>,
  notFoundComponent: () => <h1>404: Page Not Found</h1>,
  shellComponent: RootDocument,
});

function RootDocument({ children }: { children: React.ReactNode }) {
  return (
    <html>
      <head>
        <HeadContent />
      </head>
      <body>
        <div className="p-2 flex gap-2 text-lg">
          <Link to="/" activeProps={{ className: "font-bold" }} activeOptions={{ exact: true }}>
            Home
          </Link>{" "}
          <Link
            // @ts-ignore
            to="/this-route-does-not-exist"
            activeProps={{ className: "font-bold" }}
          >
            404
          </Link>
        </div>
        <hr />
        {children}
        <TanStackRouterDevtools position="bottom-right" />
        <Scripts />
      </body>
    </html>
  );
}
```

Define meta tags, stylesheets, and scripts in the `head()` function. The `shellComponent` provides the HTML document shell that wraps all pages. Use `HeadContent` to render the head configuration and `Scripts` to inject the client-side JavaScript for hydration.

## 5. Create Page Routes

Page routes define your application pages:

```tsx [src/routes/index.tsx]
import { createFileRoute } from "@tanstack/react-router";

export const Route = createFileRoute("/")({ component: Home });

function Home() {
  return (
    <div className="p-2">
      <h3>Welcome Home!</h3>
      <a href="/api/test">/api/test</a>
    </div>
  );
}
```

## API Routes

TanStack Start supports API routes alongside page routes. Create files in `src/routes/api/` to define server endpoints that Nitro serves automatically.



## Learn More

- [TanStack Start Documentation](https://tanstack.com/start)
- [Server Entry](/docs/server-entry)

---

### 4.Examples Vite Ssr Vue Router (`docs/4.examples/vite-ssr-vue-router.md`)

---
navigation:
  category: server side rendering
icon: i-logos-vue
---

# SSR with Vue Router

> Server-side rendering with Vue Router in Nitro using Vite.



::code-tree{defaultValue="app/entry-server.ts" expandAll}

```json [package.json]
{
  "type": "module",
  "scripts": {
    "build": "vite build",
    "dev": "vite dev",
    "preview": "vite preview"
  },
  "devDependencies": {
    "@vitejs/plugin-vue": "^6.0.10",
    "nitro": "latest",
    "unhead": "^3.4.3",
    "vite": "latest",
    "vite-plugin-devtools-json": "^1.1.2",
    "vue": "^3.5.44",
    "vue-router": "^5.4.0"
  }
}
```

```json [tsconfig.json]
{
  "extends": "nitro/tsconfig"
}
```

```js [vite.config.mjs]
import vue from "@vitejs/plugin-vue";
import { defineConfig } from "vite";
import devtoolsJson from "vite-plugin-devtools-json";
import { nitro } from "nitro/vite";

export default defineConfig((_env) => ({
  plugins: [patchVueExclude(vue(), /\?assets/), devtoolsJson(), nitro()],
  environments: {
    client: { build: { rollupOptions: { input: "./app/entry-client.ts" } } },
    ssr: { build: { rollupOptions: { input: "./app/entry-server.ts" } } },
    nitro: { build: { rollupOptions: { treeshake: { moduleSideEffects: () => false } } } },
  },
}));

// Workaround https://github.com/vitejs/vite-plugin-vue/issues/677
function patchVueExclude(plugin, exclude) {
  const original = plugin.transform.handler;
  plugin.transform.handler = function (...args) {
    if (exclude.test(args[1])) return;
    return original.call(this, ...args);
  };
  return plugin;
}
```

```vue [app/app.vue]
<script setup lang="ts">
import { RouterLink, RouterView } from "vue-router";
import "./styles.css";
</script>

<template>
  <nav>
    <ul>
      <li>
        <RouterLink to="/" exact-active-class="active">Home</RouterLink>
      </li>
      <li>
        <RouterLink to="/about" active-class="active">About</RouterLink>
      </li>
    </ul>
  </nav>
  <RouterView />
</template>

<style scoped>
nav {
  background: white;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
  padding: 1rem;
}

nav ul {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  gap: 2rem;
  max-width: 800px;
  margin: 0 auto;
}

nav a {
  color: #666;
  text-decoration: none;
}

nav a:hover {
  color: #333;
}

nav a.active {
  color: #646cff;
}
</style>
```

```ts [app/entry-client.ts]
import { createSSRApp } from "vue";
import { RouterView, createRouter, createWebHistory } from "vue-router";
import { routes } from "./routes.ts";

async function main() {
  const app = createSSRApp(RouterView);
  const router = createRouter({ history: createWebHistory(), routes });
  app.use(router);

  await router.isReady();
  app.mount("#root");
}

// eslint-disable-next-line unicorn/prefer-top-level-await
main();
```

```
/* Detailed source-code truncated for AI context efficiency. */
```

```ts [app/routes.ts]
import type { RouteRecordRaw } from "vue-router";

export const routes: RouteRecordRaw[] = [
  {
    path: "/",
    name: "app",
    component: () => import("./app.vue"),
    meta: {
      assets: () => import("./app.vue?assets"),
    },
    children: [
      {
        path: "/",
        name: "home",
        component: () => import("./pages/index.vue"),
        meta: {
          assets: () => import("./pages/index.vue?assets"),
        },
      },
      {
        path: "/about",
        name: "about",
        component: () => import("./pages/about.vue"),
        meta: {
          assets: () => import("./pages/about.vue?assets"),
        },
      },
      {
        path: "/:catchAll(.*)",
        name: "not-found",
        component: () => import("./pages/not-found.vue"),
        meta: {
          assets: () => import("./pages/not-found.vue?assets"),
        },
      },
    ],
  },
];
```

```ts [app/shims.d.ts]
declare module "*.vue" {
  import type { DefineComponent } from "vue";
  const component: DefineComponent<{}, {}, any>;
  export default component;
}
```

```css [app/styles.css]
* {
  box-sizing: border-box;
}

body {
  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  background: #f5f5f5;
  color: #333;
}

main {
  max-width: 800px;
  margin: 0 auto;
  padding: 2rem;
}

h1 {
  font-size: 2.5rem;
  margin-bottom: 0.5rem;
}

.card {
  background: white;
  border-radius: 8px;
  padding: 2rem;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  margin: 2rem 0;
}

button {
  background: rgb(83, 91, 242);
  color: white;
  border: none;
  padding: 0.5rem 1rem;
  border-radius: 4px;
  font-size: 1rem;
  cursor: pointer;
}

button:hover {
  background: #535bf2;
}

.subtitle {
  color: #666;
  font-size: 1.1rem;
  margin-bottom: 2rem;
}
```

```vue [app/pages/about.vue]
<template>
  <main>
    <h1>About</h1>
    <div class="card">
      <p>This is a simple Vue Router demo app built with Vite Plugin Fullstack.</p>
      <p>It demonstrates basic routing and server-side rendering.</p>
    </div>
  </main>
</template>
```

```vue [app/pages/index.vue]
<script setup lang="ts">
import { ref } from "vue";

const count = ref(0);

function increment() {
  count.value++;
}
</script>

<template>
  <main>
    <div class="hero">
      <h1>Vue Router Custom Framework</h1>
      <p class="subtitle">A simple demo app with Vite</p>
    </div>

    <div class="card counter-card">
      <p>Count: {{ count }}</p>
      <button @click="increment">Increment</button>
    </div>
  </main>
</template>

<style scoped>
.hero {
  text-align: center;
  margin-bottom: 2rem;
}

.hero h1 {
  color: rgb(100, 108, 255);
}

.counter-card {
  text-align: center;
}

.counter-card h2 {
  color: #646cff;
  margin-bottom: 1rem;
}

.counter-card p {
  font-size: 1.5rem;
  font-weight: bold;
  margin: 1rem 0;
}
</style>
```

```vue [app/pages/not-found.vue]
<template>
  <main>
    <h1>Not Found 404</h1>
  </main>
</template>
```

::





Set up server-side rendering (SSR) with Vue, Vue Router, Vite, and Nitro. This setup enables per-route code splitting, head management with unhead, and client hydration.

## Overview

1. Add the Nitro Vite plugin to your Vite config
2. Define routes with lazy-loaded components
3. Create a server entry that renders your app with router support
4. Create a client entry that hydrates and takes over routing
5. Create page components

## 1. Configure Vite

Add the Nitro and Vue plugins to your Vite config. Define both `client` and `ssr` environments:

```js [vite.config.mjs]
import vue from "@vitejs/plugin-vue";
import { defineConfig } from "vite";
import devtoolsJson from "vite-plugin-devtools-json";
import { nitro } from "nitro/vite";

export default defineConfig((_env) => ({
  plugins: [patchVueExclude(vue(), /\?assets/), devtoolsJson(), nitro()],
  environments: {
    client: { build: { rollupOptions: { input: "./app/entry-client.ts" } } },
    ssr: { build: { rollupOptions: { input: "./app/entry-server.ts" } } },
  },
}));

// Workaround https://github.com/vitejs/vite-plugin-vue/issues/677
function patchVueExclude(plugin, exclude) {
  const original = plugin.transform.handler;
  plugin.transform.handler = function (...args) {
    if (exclude.test(args[1])) return;
    return original.call(this, ...args);
  };
  return plugin;
}
```

The `patchVueExclude` helper prevents the Vue plugin from processing asset imports (files with `?assets` query parameter).

## 2. Define Routes

Create route definitions with lazy-loaded components and asset metadata:

```ts [app/routes.ts]
import type { RouteRecordRaw } from "vue-router";

export const routes: RouteRecordRaw[] = [
  {
    path: "/",
    name: "app",
    component: () => import("./app.vue"),
    meta: {
      assets: () => import("./app.vue?assets"),
    },
    children: [
      {
        path: "/",
        name: "home",
        component: () => import("./pages/index.vue"),
        meta: {
          assets: () => import("./pages/index.vue?assets"),
        },
      },
      {
        path: "/about",
        name: "about",
        component: () => import("./pages/about.vue"),
        meta: {
          assets: () => import("./pages/about.vue?assets"),
        },
      },
      {
        path: "/:catchAll(.*)",
        name: "not-found",
        component: () => import("./pages/not-found.vue"),
        meta: {
          assets: () => import("./pages/not-found.vue?assets"),
        },
      },
    ],
  },
];
```

Use dynamic imports for lazy-loaded components to enable code splitting. The `meta.assets` function loads route-specific CSS and JS chunks. Define child routes under a root layout component for nested routing.

## 3. Create the Server Entry

The server entry renders your Vue app with router support and head management:

```
/* Detailed source-code truncated for AI context efficiency. */
```

The server uses `createMemoryHistory()` since there's no browser URL bar—the router navigates to the requested URL before rendering. Assets are loaded dynamically based on matched routes, ensuring only the CSS and JS needed for the current page are included. The `unhead` library manages `<head>` elements, injecting stylesheets and scripts via `transformHtmlTemplate`.

## 4. Create the Client Entry

The client entry hydrates the server-rendered HTML and takes over routing:

```ts [app/entry-client.ts]
import { createSSRApp } from "vue";
import { RouterView, createRouter, createWebHistory } from "vue-router";
import { routes } from "./routes.ts";

async function main() {
  const app = createSSRApp(RouterView);
  const router = createRouter({ history: createWebHistory(), routes });
  app.use(router);

  await router.isReady();
  app.mount("#root");
}

// eslint-disable-next-line unicorn/prefer-top-level-await
main();
```

The client entry creates a Vue app with `createWebHistory()` for browser-based routing. After the router is ready, it mounts to the `#root` element and hydrates the server-rendered HTML.

## 5. Create the Root Component

The root component provides navigation and renders child routes:

```vue [app/app.vue]
<script setup lang="ts">
import { RouterLink, RouterView } from "vue-router";
import "./styles.css";
</script>

<template>
  <nav>
    <ul>
      <li>
        <RouterLink to="/" exact-active-class="active">Home</RouterLink>
      </li>
      <li>
        <RouterLink to="/about" active-class="active">About</RouterLink>
      </li>
    </ul>
  </nav>
  <RouterView />
</template>

<style scoped>
nav {
  background: white;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
  padding: 1rem;
}

nav ul {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  gap: 2rem;
  max-width: 800px;
  margin: 0 auto;
}

nav a {
  color: #666;
  text-decoration: none;
}

nav a:hover {
  color: #333;
}

nav a.active {
  color: #646cff;
}
</style>
```



## Learn More

- [Vue Router Documentation](https://router.vuejs.org/)
- [Unhead Documentation](https://unhead.unjs.io/)
- [Renderer](/docs/renderer)
- [Server Entry](/docs/server-entry)

---

--- METRICS ---
- Files Extracted: 100
- Estimated Token Budget: ~125809 tokens
- Canonical Reference: https://codewiki.google/github.com/nitrojs/nitro
