# Nitro > Add server API routes to any Vite apps and deploy with zero configuration on your favorite hosting platform. --- # Home ::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="nitro@latest create"} #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 --- # 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). --- # 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 the `nitro create` command. > [!NOTE] > Make sure you have the latest LTS version of [Node.js](https://nodejs.org/en) or [Bun](https://bun.sh/) installed. ::code-group ```sh [npm] npx --min-release-age=0 nitro@latest create ``` ```sh [pnpm] pnpm dlx --config.minimum-release-age=0 nitro@latest create ``` :: ::note The `--min-release-age=0` (npm) and `--config.minimum-release-age=0` (pnpm) flags skip any minimum release age you have configured, so you always get the latest Nitro release. :: Follow the CLI instructions and you will be ready to start your development server. See [`nitro create`](/docs/cli#nitro-create) for all options. ## 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). --- # 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] My Vite + Nitro App
``` ```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] Custom Template
Loading...
``` :: ### 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 ``: raw (unescaped) output ```html

Hello {{ $URL.pathname }}

{{{ 'raw html' }}}
``` #### Control flow Use `` for JavaScript control flow: ```html

Form submitted!

``` #### Server scripts Use `
{{ JSON.stringify(data) }}
``` #### Streaming content Use the `echo()` function for streaming content. It accepts strings, functions, Promises, Response objects, or ReadableStreams: ```html ``` #### 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] Dynamic template

Hello {{ $REQUEST.url }}

Welcome, !

``` :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 */ ` Custom Renderer

Hello from custom renderer!

Current path: ${url.pathname}

`, { 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] SSR App
``` ### 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. --- # 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 `[]` syntax where `` 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 `[]/[]` 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 `[...]` 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/directories#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" }; } }); ``` ::warning Middleware are not a security boundary. Global and route-scoped middleware are **implicit** guards: they protect the URLs they happen to match, not the handlers themselves. A path check like `startsWith('/auth')` also matches `/authors`, and any route added outside the pattern is silently left unprotected. Use them for cross-cutting concerns (logging, context, defense in depth), and enforce access control explicitly in the handler. See [Authentication](#authentication). :: ### 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/build#inlinedynamicimports) to bundle everything into a single file. ## Route rules Route rules let you attach behavior (redirects, proxying, caching, CORS, 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/**' }, } }); ``` ::warning Route rules are not a security boundary. Like global middleware, they are **implicit** guards: they apply to whatever request URLs match a pattern, not to the handlers they are meant to protect. A rule can be overridden by a more specific one, and a handler stays unprotected when it is reachable from a path the pattern does not match (a new route, a renamed file, a `proxy` target, a different `baseURL`). Some rules are also applied by the platform edge and others only by the server runtime. Enforce access control explicitly where the protected code runs. See [Authentication](#authentication). :: ::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 serve public and prerendered assets straight from the CDN, those responses only get the platform's default headers (e.g. Vercel sends `access-control-allow-origin: *`, Netlify sends none). To set CORS headers there, also add them with `headers`, which are written to the platform config: > > ```ts > '/**': { cors: true, headers: { 'access-control-allow-origin': '*' } }, > ``` 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/routing#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. Route rules and middleware match request URLs, so they can only guard routes [implicitly](#route-rules). Instead, enforce access control **explicitly** in the code you want to protect, using composable h3 utilities such as [`requireBasicAuth`](https://h3.dev/utils/security#requirebasicauthevent-opts). Wrap the check in a small reusable utility, reading the credentials from environment variables rather than hardcoding them: ```ts [utils/auth.ts] import type { H3Event } from "nitro"; import { requireBasicAuth } from "nitro/h3"; export async function requireAdmin(event: H3Event) { await requireBasicAuth(event, { username: process.env.ADMIN_USERNAME, password: process.env.ADMIN_PASSWORD!, realm: "Admin Area", }); return event.context.basicAuth!.username; } ``` Then call it at the top of each handler that needs it. The request fails with `401` before any protected logic runs (or with `500` if `ADMIN_PASSWORD` is not set): ```ts [routes/admin/index.ts] import { defineHandler } from "nitro"; import { requireAdmin } from "../../utils/auth.ts"; export default defineHandler(async (event) => { const username = await requireAdmin(event); return `Hello, ${username}!`; }); ``` The guard is visible next to the code it protects and keeps working wherever the handler is mounted or however it is reached. The same utility can be reused by any handler, or called only on the code paths that need it. To attach the guard declaratively, pass it to the handler's `middleware` option. Like the call above, it stays bound to the handler rather than to a URL pattern. Middleware should not return a value, so wrap the utility: ```ts [routes/admin/settings.ts] import { defineHandler } from "nitro"; import { requireAdmin } from "../../utils/auth.ts"; export default defineHandler({ middleware: [ async (event) => { await requireAdmin(event); }, ], handler: (event) => `Settings for ${event.context.basicAuth?.username}`, }); ``` h3 also provides a ready-made [`basicAuth`](https://h3.dev/utils/security#basicauthopts) middleware factory (`middleware: [basicAuth({ ... })]`) for the same purpose. ### 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: ` is a shortcut for `cache: { swr: true, maxAge: }`. :: ### 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` | 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` | `{}` | Route-to-handler mapping | | `handlers` | `NitroEventHandler[]` | `[]` | Programmatic handler registration (mainly for middleware) | | `routeRules` | `Record` | `{}` | Route rules for matching patterns | | `ignore` | `string[]` | `[]` | Glob patterns to ignore during file scanning | --- # 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. A `fetch` handler is optional. A server entry that only runs startup logic or sets [server options](#server-options) can leave it out, or have no default export at all. Requests no route matched then go to the renderer, or get a `404` response. This does not apply to [Node.js format](#nodejs-frameworks) entries (`server.node.ts`), which must default export a `(req, res)` handler. ```ts [server.ts] import { defineServerEntry } from "nitro"; console.log("Server is starting..."); export default defineServerEntry({ port: 8080, }); ``` 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 --- # 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_` (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) => 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"} A function that returns a `boolean` to invalidate the current cache and create a new one. :: ::field{name="shouldBypassCache()" type="(...args) => boolean | Promise"} 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"} 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"} 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.` 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/storage#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. :: --- # 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/storage#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/storage#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/directories#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:`: ```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. :: --- # 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` with `Cache-Control: no-store` is returned immediately. This keeps the `maxAge` cache header of the directory off missing files. ### 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=, 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. :: ## Files Referenced by URL Files referenced relative to a module with `new URL("./file", import.meta.url)` are copied to the server output (`.output/server/assets/`), and the URL is rewritten to point to the copy. They are not inlined, so this also works for large files, such as the `.wasm` and `.data` files of packages built with emscripten or wasm-bindgen (for example [`@electric-sql/pglite`](https://pglite.dev)). ```ts [routes/model.ts] import { readFile } from "node:fs/promises"; import { defineHandler } from "nitro"; export default defineHandler(async () => { const model = await readFile(new URL("./model.bin", import.meta.url)); return { size: model.byteLength }; }); ``` ::note This is supported for presets with filesystem access (Node.js, Bun, Deno and the Node.js functions of Vercel and Netlify). The URL must be a string literal and `import.meta.url` the second argument. JavaScript and TypeScript files, URLs with a query or hash, and files that don't exist at build time are left as they are. With Vite, this applies to the Nitro environment, including the dependencies of service environments (such as `ssr`), which Nitro bundles. Files referenced by the source of a service environment itself are not copied. In development, modules are loaded from their original location, so URLs are not rewritten. :: ## Server Assets All assets in the `assets/` directory (relative to [`serverDir`](/config/directories#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:`). - `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. :: --- # 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`. Use the `--envName` flag to set a custom name, and the `$env` key for its overrides: ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ $env: { staging: { // Options applied only with `--envName staging` runtimeConfig: { apiBase: "https://staging.example.com" }, }, }, }) ``` ```sh npx nitro build --envName staging ``` Routes prerendered during that build apply the same overrides. Tools that use the [`nitro/builder`](/docs/utils#nitrobuilder) API can pass the name with `createNitro(config, { c12: { envName: "staging" } })`. The resolved name is available as `nitro.options.envName`, and as `import.meta.envName` in server code (replaced statically at build time): ```ts [routes/env.ts] import { defineHandler } from "nitro"; export default defineHandler(() => ({ env: import.meta.envName })); ``` ### 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"`. --- # Database > Nitro provides a built-in and lightweight SQL database layer. > 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(); ``` When the experimental [`tracingChannel`](/config/advanced#tracingchannel) option is enabled, queries made with `exec()`, `sql` and prepared statements on `useDatabase()` instances are published on the `db0.query` tracing channel. Queries made through `getInstance()`, or by an ORM using the underlying client, are not traced. ::note Nitro's built-in tracing sinks (the [console logger](/config/features#tracinglogger), Vercel and Cloudflare) record the query method and SQL dialect, not the query text. The channel message itself includes the full query text, so any other subscriber, such as an APM agent, receives it. Failed queries are recorded with their error message, which some databases build from parts of the query. :: ## 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. --- # 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` | Start of each request, before routing. | | `response` | `(res: Response, event: HTTPEvent) => void \| Promise` | 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. :: --- # 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. :: --- # 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/directories#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` | The app's internal fetch handler. | | `captureError` | `(error: Error, context) => void` | Programmatically capture errors into the error hook pipeline. | | `ready` | `Promise \| 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` | Called at the start of each request. | | `response` | `(res: Response, event: HTTPEvent) => void \| Promise` | 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"); } }); }); ``` --- # 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 route pattern, 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 "/blog/**", // Ignore /blog and everything under it "/users/:id", // Ignore /users/1, but not /users/1/posts /\/preview$/, // Ignore routes ending with /preview (path) => path.includes("draft"), // Ignore routes containing "draft" ], }, }); ``` A string containing `*` or `:` is matched as a [rou3](https://github.com/h3js/rou3) route pattern against the path (without its query string), the same syntax as [route rules](/docs/routing#route-rules). Any other string is matched as a prefix, so `"/admin"` also ignores `/admin-panel`, while `"/admin/**"` does not. ::note `*` and `**` both match across segments: `"/blog/*"` ignores `/blog/a/b` too. Both also match the base path itself (`/blog`). `:name` matches exactly one segment. :: 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. Independently of this option, the build always fails if every prerendered route failed and nothing was written to the public directory. | | `ignoreStatusCodes` | `[]` | Status codes that do not fail the build when `failOnError` is enabled, for example `[404]` to allow broken links. Failed routes with those status codes are still reported and not written. | | `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, ignoreStatusCodes: [404], 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. :: --- # 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 { 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`) 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. --- # 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` | All connected peers in the same namespace | | `topics` | `Set` | 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"} --- # 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) => 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. --- # 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 except `nitro create` 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. `nitro fetch` only accepts `--dir`, as its positional argument is the URL. ::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 create` Create a new Nitro project from a template. Nitro ships the templates itself, so no download is needed, and the `nitro` dependency of the new project is set to the version that created it. ```sh npx nitro create [dir] [--template