# Nitro
> Add server API routes to any Vite apps and deploy with zero configuration on your favorite hosting platform.
---
# 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, code-splitting, and optimized builds.
You can also take full control of the server entry by creating a `server.ts` file. Nitro’s high-level, 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 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 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. 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 using the `create-nitro-app`.
> [!NOTE]
> Make sure to have installed the latest LTS version of either [Node.js](https://nodejs.org/en), [Bun](https://bun.sh/), or [Deno](https://deno.com/).
:pm-x{command="create-nitro-app"}
Preview
Follow the instructions from the CLI and you will be ready to start your development server.
## Add to a Vite project
You can add Nitro to any existing Vite project to get API routes, server-side rendering, and more.
::steps{level="3"}
### Install `nitro` and `vite`
:pm-install{name="nitro vite"}
### Add Nitro plugin to Vite
Add the Nitro plugin to your `vite.config.ts`:
```ts [vite.config.ts] {2,6}
import { defineConfig } from "vite";
import { nitro } from "nitro/vite";
export default defineConfig({
plugins: [
nitro()
],
});
```
### Configure Nitro
Create a `nitro.config.ts` to configure the server directory:
```ts [nitro.config.ts]
import { defineConfig } from "nitro";
export default defineConfig({
serverDir: "./server",
});
```
The `serverDir` option tells Nitro where to look for your server routes. In this example, all routes will be inside the `server/` directory.
### 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`.
::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.
::
::tip
You can return strings, JSON objects, `Response` instances, or readable streams from your handlers. See [Routing](/docs/routing) for more about dynamic routes, methods, and middleware.
::
### Start the development server
:pm-run{script="dev -- --open"}
Your API route is now accessible at `http://localhost:3000/api/test` :sparkles:
---
# 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, // Treat template as static HTML (no rendu processing)
}
})
```
| 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 src dir.
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)
Nitro uses [rendu](https://github.com/h3js/rendu) Hypertext Preprocessor, which provides a simple and powerful way to create dynamic HTML templates with JavaScript expressions.
#### Output expressions
- `{{ expression }}` — HTML-escaped output
- `{{{ expression }}}` or `= expression ?>` — raw (unescaped) output
```html
Hello {{ $URL.pathname }}
{{{ 'raw html' }}}
```
#### Control flow
Use ` ... ?>` for JavaScript control flow:
```html
if ($METHOD === 'POST') { ?>
Form submitted!
} else { ?>
} ?>
for (const item of ['a', 'b', 'c']) { ?>
{{ item }}
} ?>
```
#### 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, = $COOKIES["user"] || "Guest" ?>!
```
: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)
```
::warning
If you define a catch-all route (`[...].ts`) in your routes, Nitro will warn you that the renderer will override it. Use more specific routes or different HTTP methods to avoid conflicts.
::
: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. By combining code-splitting with compiled routes, it removes the need for a runtime router, leaving only minimal compiled logic.
## Request handler
Nitro request handler is a function accepting an `event` object, which is a [H3Event](https://h3.dev/guide/api/h3event#h3event-properties) object.
::code-group
```ts [Single function]
import type { H3Event } from "nitro";
export default (event: H3Event) => {
return "world";
}
```
```ts [defineHandler]
import { defineHandler } from "nitro";
// For better type inference
export default defineHandler((event) => {
return "world";
});
```
::
## Filesystem routing
Nitro supports file-based routing for your API routes (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.
You can only define one handler per files and you can [append the HTTP method](#specific-request-method) to the filename to define 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
In some cases, you may want to group a set of routes together in a way which doesn't affect file-based routing. For this purpose, you can put files in a folder which is 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] The route groups are not part of the route definition and are only used for organization purposes.
### Static routes
First, create a file in `routes/` or `routes/api/` directory. The filename will be the route path.
Then, export a fetch-compatible function:
```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 in the `event.context.params` object or using the [`getRouterParam`](https://h3.dev/utils/request#getrouterparamevent-name-opts-decode) utility.
```ts [routes/hello/[name\\].ts]
import { defineHandler } from "nitro";
export default defineHandler((event) => {
const { name } = event.context.params;
return `Hello ${name}!`;
});
```
Call the route with the param `/hello/nitro`, you will get:
```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 of 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}!`;
});
```
Call the route with the param `/hello/nitro/is/hot`, you will get:
```txt [Response]
Hello nitro/is/hot!
```
### 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`, `connect`, `trace`.
::code-group
```js [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!`;
});
```
```js [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 will match all routes that are not matched by any other route. This is useful for creating a default 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}!`;
});
```
### Environment specific handlers
You can specify for a route that will only be included in specific builds by adding a `.dev`, `.prod` or `.prerender` suffix to the file name, 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]
> You can specify multiple environments or specify a preset name as environment using programmatic registration of routes via [`routes`](#routes-config) config.
### Ignoring files
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 _
],
});
```
## 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/middleware/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
Nitro route middleware can hook into the request lifecycle.
::tip
A middleware can modify the request before it is processed, not after.
::
Middleware are auto-registered within the `middleware/` directory.
```md
middleware/
auth.ts
logger.ts
...
routes/
hello.ts
```
### Simple middleware
Middleware are defined exactly like route handlers with the only exception that they should not return anything.
Returning from middleware behaves like returning from a request - the value will be returned as a response and further code will not be ran.
```ts [middleware/auth.ts]
import { defineHandler } from "nitro";
export default defineHandler((event) => {
// Extends or modify the event
event.context.user = { name: "Nitro" };
});
```
Middleware in `middleware/` directory are automatically registered for all routes. If you want to register a middleware for a specific route, see [Object Syntax Event Handler](https://h3.dev/guide/basics/handler#object-syntax).
::note
Returning anything from a middleware will close the request and should be avoided! Any returned value from middleware will be the response and further code will not be executed however **this is not recommended to do!**
::
### Route Meta
You can define route handler meta at build-time using `defineRouteMeta` macro in the event 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 swagger specification for available OpenAPI options.
::
### 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
Remember that file names are sorted as strings, thus for example if you have 3 files `1.filename.ts`, `2.filename.ts` and `10.filename.ts`, the `10.filename.ts` will come after the `1.filename.ts`. To avoid this, prefix `1-9` with a `0` like `01`, if you have more than 10 middleware in the same directory.
::
### Request filtering
Middleware are executed on every request.
Apply custom logic to scope them to specific conditions.
For example, you can use the URL to apply a middleware to a specific route:
```ts [middleware/auth.ts]
import { defineHandler } from "nitro";
export default defineHandler((event) => {
// Will only execute for /auth route
if (event.url.pathname.startsWith('/auth')) {
event.context.user = { name: "Nitro" };
}
});
```
### Route-scoped middleware
You can register middleware for specific route patterns using the [`handlers`](#handlers-config) config with the `middleware` option and a specific `route`:
```ts [nitro.config.ts]
import { defineConfig } from "nitro";
export default defineConfig({
handlers: [
{
route: "/api/**",
handler: "./server/middleware/api-auth.ts",
middleware: true,
},
],
});
```
Unlike global middleware (registered in the `middleware/` directory which match `/**`), route-scoped middleware only run for requests matching the specified pattern.
## Error handling
You can use the [utilities available in H3](https://h3.dev/guide/basics/error) to handle errors in both routes and middlewares.
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) will receive a HTML error page. In production, errors are always sent in JSON.
This behaviour can be overridden by some request properties (e.g.: `Accept` or `User-Agent` headers).
## Code splitting
Nitro creates a separate chunk for each route handler. Chunks load on-demand when first requested, so `/api/users` doesn't load code for `/api/posts`.
See [`inlineDynamicImports`](/config#inlinedynamicimports) to bundle everything into a single file.
## Route rules
Nitro allows you to add logic at the top-level for each route of your configuration. It can be used for redirecting, proxying, caching, authentication, and adding headers to routes.
It is a map from route pattern (following [rou3](https://github.com/h3js/rou3)) to route options and based on [`h3-rules`](https://github.com/h3js/h3-rules).
When `cache` option is set, handlers matching pattern will be automatically wrapped with `defineCachedHandler`. See the [cache guide](/docs/cache) to learn more about this function.
::note
`swr: true|number` is shortcut for `cache: { swr: true, maxAge: number }`
::
You can set route rules in the `nitro.routeRules` options.
```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/**' },
'/admin/**': { basicAuth: { username: 'admin', password: 'supersecret' } },
}
});
```
### 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 },
'/admin/**': { basicAuth: { username: 'admin', password: 'secret' } },
'/admin/public/**': { basicAuth: 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/** additionally require auth
'POST /api/**': { basicAuth: { username: 'admin', password: 'secret' } },
// Cache GET requests to /feed only
'GET /feed': { swr: 600 },
}
});
```
::note
Method matching is resolved per request by the server runtime. Platform-native static generation (e.g. Netlify/Cloudflare `_headers` & `_redirects`, Vercel `config.json`) does not split by method, so prefer method-agnostic keys for `headers`/`redirect`/`proxy` rules you expect a platform to emit into its static config.
::
### Headers
Set custom response headers for matching routes:
```ts [nitro.config.ts]
import { defineConfig } from "nitro";
export default defineConfig({
routeRules: {
'/api/**': { headers: { 'cache-control': 's-maxage=60' } },
'**': { headers: { 'x-powered-by': 'Nitro' } },
}
});
```
### CORS
Handle CORS at runtime with the `cors` rule. `cors: true` applies permissive defaults (origin, methods, and allowed headers `*`): a simple request gets `access-control-allow-origin: *` and `access-control-expose-headers: *`, and an `OPTIONS` preflight is answered directly (`204`) with the matching `access-control-allow-*` headers.
> [!NOTE]
> CORS is applied by the running server (h3's [`handleCors`](https://h3.dev/utils/security#handlecorsevent-options)), and not from the static/CDN config.
> On platforms that can serve prerendered/static assets straight from the edge, CORS headers are only added when the request reaches the server handler.
Pass an object for finer control — an origin allowlist, `credentials`, `maxAge`, etc. (h3 `CorsOptions`). Combining `credentials: true` with a wildcard origin is invalid and throws at build time:
```ts [nitro.config.ts]
import { defineConfig } from "nitro";
export default defineConfig({
routeRules: {
// Permissive defaults
'/api/public/**': { cors: true },
// Restrict to specific origins with credentials
'/api/v1/**': {
cors: { origin: ['https://app.example.com'], credentials: true },
},
}
});
```
### Redirect
Redirect matching routes to another URL. Use a string for a simple redirect (defaults to `307` status), or an object for more control:
```ts [nitro.config.ts]
import { defineConfig } from "nitro";
export default defineConfig({
routeRules: {
// Simple redirect (307 status)
'/old-page': { redirect: '/new-page' },
// Redirect with custom status
'/legacy': { redirect: { to: 'https://example.com/', status: 308 } },
// Wildcard redirect — preserves the path after the pattern
'/old-blog/**': { redirect: 'https://blog.example.com/**' },
}
});
```
### 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...
},
},
}
});
```
### Basic auth
Protect routes with HTTP Basic Authentication:
```ts [nitro.config.ts]
import { defineConfig } from "nitro";
export default defineConfig({
routeRules: {
'/admin/**': {
basicAuth: {
username: 'admin',
password: 'supersecret',
realm: 'Admin Area', // Optional, shown in the browser prompt
},
},
// Disable basic auth for a sub-path
'/admin/public/**': { basicAuth: false },
}
});
```
### 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 },
}
});
```
### 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,
},
},
}
});
```
### 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 |
| `basicAuth` | `{ username, password, realm? } \| false` | HTTP Basic Authentication |
| `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 create a global middleware that runs for all routes before they are matched.
The server entry is a special handler in Nitro that acts as a global middleware, running for every incoming request before routes are matched. It's commonly used for cross-cutting concerns like authentication, logging, request preprocessing, or creating custom routing logic.
## Auto-detected `server.ts`
By default, Nitro automatically looks for a `server.ts` (or `.js`, `.mjs`, `.mts`, `.tsx`, `.jsx`) file in 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;
```
::
::
## 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 event handler
You can also export an event handler using `defineHandler` for better type inference and access to the h3 event object:
```ts [server.ts]
import { defineHandler } from "nitro";
export default defineHandler((event) => {
// Add custom context
event.context.requestId = crypto.randomUUID();
event.context.timestamp = Date.now();
// Log the request
console.log(`[${event.context.requestId}] ${event.method} ${event.path}`);
// Continue to the next handler (don't return anything)
});
```
::important
If your server entry returns `undefined` or doesn't return anything, the request will continue to be processed by routes and the renderer. If it returns a response, the request lifecycle stops there.
::
## 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 (middleware/)
4. 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 exist, they are chained: the server entry runs first, and if it doesn't return a response, the renderer handles the request.
## 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 server entry for cross-cutting concerns that affect **all routes**
- Return `undefined` to continue processing, return a response to terminate
- Keep server entry logic lightweight for better performance
- Use global middleware for modular concerns instead of one large server entry
- Consider using [Nitro plugins](/docs/plugins) for initialization logic
- Avoid heavy computation in server entry (it runs for every request)
- Don't use server entry for route-specific logic (use route handlers instead as they are more performant)
---
# Cache
> Nitro provides a caching system built on top of the storage layer, powered by [ocache](https://github.com/unjs/ocache).
## Cached handlers
To cache an event handler, you simply need to use the `defineCachedHandler` method.
It works like `defineHandler` but with an 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 and a stale value will be sent to the client while the cache is being updated in the background. If you want to immediately return the updated response set `swr: false`.
See the [options](#options) section for more details about the available options.
::important
**Request headers are dropped** when handling cached responses. Use the [`varies` option](#options) to consider specific headers when caching and serving the responses.
::
### 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.
- **`last-modified`** -- Set to the current time when the response is first cached, if not already set.
- **`cache-control`** -- Automatically set based on the `swr`, `maxAge`, and `staleMaxAge` options:
- With `swr: true`: `s-maxage=, stale-while-revalidate=`
- With `swr: false`: `max-age=`
### Conditional requests (304 Not Modified)
Cached handlers automatically support conditional requests. When a client sends `if-none-match` or `if-modified-since` headers matching the cached response, Nitro returns a `304 Not Modified` response without a body.
### Request method filtering
Only `GET` and `HEAD` requests are cached. All other HTTP methods (`POST`, `PUT`, `DELETE`, etc.) automatically bypass the cache and call the handler directly.
### 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.
## Cached functions
You can also cache a function using the `defineCachedFunction` function. 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 in development inside `.nitro/cache/functions/ghStars//.json` 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...
::
::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";
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.
::
## Using route rules
This feature enables you to add caching routes based on a glob pattern directly in the main configuration file. This is especially useful to have a global cache strategy for a part of your application.
Cache all the blog routes for 1 hour with `stale-while-revalidate` behavior:
```ts [nitro.config.ts]
import { defineConfig } from "nitro";
export default defineConfig({
routeRules: {
"/blog/**": { cache: { maxAge: 60 * 60 } },
},
});
```
If we want to use a [custom cache storage](#cache-storage) mount point, we can use the `base` option.
```ts [nitro.config.ts]
import { defineConfig } from "nitro";
export default defineConfig({
storage: {
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. When set to `true`, SWR is enabled with the default `maxAge`. When set to a number, it is used as the `maxAge` value in seconds.
```ts [nitro.config.ts]
import { defineConfig } from "nitro";
export default defineConfig({
routeRules: {
"/blog/**": { swr: true },
"/api/**": { swr: 3600 },
},
});
```
To explicitly disable caching on a route, set `cache: false`:
```ts [nitro.config.ts]
import { defineConfig } from "nitro";
export default defineConfig({
routeRules: {
"/api/realtime/**": { cache: false },
},
});
```
::note
When using route rules, cached handlers use the group `'nitro/route-rules'` instead of the default `'nitro/handlers'`.
::
## Cache storage
Nitro stores the data in the `cache` storage mount point.
- In production, it will use the [memory driver](https://unstorage.unjs.io/drivers/memory) by default.
- In development, it will use the [filesystem driver](https://unstorage.unjs.io/drivers/fs), writing to a temporary dir (`.nitro/cache`).
To overwrite the production storage, set the `cache` mount point using the `storage` option:
```ts [nitro.config.ts]
import { defineConfig } from "nitro";
export default defineConfig({
storage: {
cache: {
driver: 'redis',
/* redis connector options */
}
}
})
```
In development, you can also overwrite the cache mount point using the `devStorage` option:
```ts [nitro.config.ts]
import { defineConfig } from "nitro";
export default defineConfig({
storage: {
cache: {
// production cache storage
},
},
devStorage: {
cache: {
// development cache storage
}
}
})
```
## 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"}
Name of the storage mountpoint to use for caching. :br
Default to `cache`.
::
::field{name="name" type="string"}
Guessed from function name if not provided, and falls back to `'_'` 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 URL path and search params.
::
::field{name="integrity" type="string"}
A value that invalidates the cache when changed. :br
By default, it is computed from **function code**, used in development to invalidate the cache when the function code changes.
::
::field{name="maxAge" type="number"}
Maximum age that cache is valid, in seconds. :br
Default to `1` (second).
::
::field{name="staleMaxAge" type="number"}
Maximum age that a stale cache is valid, in seconds. If set to `-1` a stale value will still be sent to the client while the cache updates in the background. :br
Defaults to `0` (disabled).
::
::field{name="swr" type="boolean"}
Enable `stale-while-revalidate` behavior to serve a stale cached response while asynchronously revalidating it. :br
When enabled, stale cached values are returned immediately while revalidation happens in the background. When disabled, the caller waits for the fresh value before responding (the stale entry is cleared). :br
Defaults to `true`.
::
::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.
::
::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 but benefits from conditional caching.
::
::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. :br :br
Headers **not** listed in `varies` are stripped from the request before calling the handler to ensure consistent cache hits. :br :br
For multi-tenant environments, you may want to pass `['host', 'x-forwarded-host']` to ensure these headers are not discarded and that the cache is unique per tenant.
::
::
### 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.
::
::field{name="validate()" type="(entry: CacheEntry, ...args) => boolean"}
Validate a cache entry. Return `false` to treat the entry as invalid and trigger re-resolution.
::
::
## SWR behavior
The `stale-while-revalidate` (SWR) pattern is enabled by default (`swr: true`). Understanding how it interacts with other options:
| `swr` | `maxAge` | Behavior |
|-------|----------|----------|
| `true` (default) | `1` (default) | Cache for 1 second, serve stale while revalidating |
| `true` | `3600` | Cache for 1 hour, serve stale while revalidating |
| `false` | `3600` | Cache for 1 hour, wait for fresh value when expired |
| `true` | `3600` with `staleMaxAge: 600` | Cache for 1 hour, serve stale for up to 10 minutes while revalidating |
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 and a cached value has expired:
1. The stale entry is cleared.
2. The client waits for the function/handler to resolve with a fresh value.
## 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()` method
Every function created with `defineCachedFunction` exposes an `.invalidate()` method. 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.invalidate("unjs/nitro"); // removes the entry
await cachedGHStars("unjs/nitro"); // re-invokes the function
```
If no cached entry matches the given arguments, `.invalidate()` resolves without error and leaves storage unchanged.
### `invalidateCache()` helper
`invalidateCache` is a helper from `ocache`. It invalidates a cached entry from the cache options used to define it, so invalidation can live anywhere in your app, independent of where the cached function is defined.
Pass the same `name`, `group`, `base`, and `getKey` used when defining the function, along with the `args` identifying the entry to remove:
```ts
import { invalidateCache } from "ocache";
await invalidateCache({
options: {
name: "ghStars",
group: "nitro/functions",
getKey: (repo: string) => repo,
},
args: ["unjs/nitro"],
});
```
::important
The `name`, `group`, `base`, and `getKey` passed to `invalidateCache` must match the ones used when the cached function was defined. Mismatched options resolve to a different storage key and will not invalidate the intended entry. :br :br
Nitro defaults to `group: 'nitro/functions'` for cached functions and `group: 'nitro/handlers'` for cached handlers (or `'nitro/route-rules'` when using [route rules](#using-route-rules)).
::
## 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`
```
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 generate the following cache key:
```ts
cache:nitro/functions:getAccessToken:default.json
```
::note
For cached handlers, the cache key includes a hash of the URL path and, when using the [`varies`](#handler-only-options) option, hashes of the specified header values appended to the key.
::
::note
Responses with HTTP status codes `>= 400` or with an undefined body are not cached. This prevents caching error responses.
::
::read-more{to="/docs/storage"}
Read more about the Nitro storage.
::
---
# KV Storage
> Nitro provides a built-in storage layer that can abstract filesystem or database or any other data source.
Nitro has built-in integration with [unstorage](https://unstorage.unjs.io) to provide a runtime agnostic persistent layer.
## Usage
To use the storage layer, you can use the `useStorage()` utility to access the storage instance.
```ts
import { useStorage } from "nitro/storage";
// Default storage (in-memory)
await useStorage().setItem("test:foo", { hello: "world" });
const value = await useStorage().getItem("test:foo");
// You can specify a base prefix with useStorage(base)
const testStorage = useStorage("test");
await testStorage.setItem("foo", { hello: "world" });
await testStorage.getItem("foo"); // { hello: "world" }
// You can use generics to type the return value
await useStorage<{ hello: string }>("test").getItem("foo");
await useStorage("test").getItem<{ hello: string }>("foo");
```
:read-more{to="https://unstorage.unjs.io"}
### Available methods
The storage instance returned by `useStorage()` 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 { useStorage } from "nitro/storage";
// Get all keys under a prefix
const keys = await useStorage("test").getKeys();
// Check if a key exists
const exists = await useStorage().hasItem("test:foo");
// Remove a key
await useStorage().removeItem("test:foo");
// Get raw binary data
const raw = await useStorage().getItemRaw("assets/server:image.png");
// Get metadata (type, etag, mtime, etc.)
const meta = await useStorage("assets/server").getMeta("file.txt");
```
## Configuration
You can mount one or multiple custom storage drivers using the `storage` 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({
storage: {
redis: {
driver: "redis",
/* redis connector options */
}
}
})
```
Then, you can use the redis storage using the `useStorage("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.
::
### Development storage
You can use the `devStorage` option to override storage configuration during development and prerendering.
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({
storage: {
db: {
driver: "redis",
host: "prod.example.com",
}
},
devStorage: {
db: {
driver: "fs",
base: "./.data/db"
}
}
})
```
When running in development mode, `devStorage` mounts are merged on top of `storage` mounts, allowing you to use a local filesystem driver or an in-memory driver while developing.
## Built-in mount points
Nitro automatically mounts the following storage paths:
### `/assets`
Server assets are mounted at the `/assets` base path. This mount point provides read-only access to bundled server assets (see [Server assets](#server-assets)).
```ts
import { useStorage } from "nitro/storage";
// Access server assets via the /assets mount
const content = await useStorage("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 { useStorage } from "nitro/storage";
// In-memory by default, not persisted
await useStorage().setItem("counter", 1);
```
To persist data, mount a driver with a persistent backend (e.g., `fs`, `redis`, etc.) using the `storage` configuration option.
## Server assets
Nitro allows you to bundle files from an `assets/` directory at the root of your project. These files are accessible at runtime via the `assets/server` storage mount.
```
my-project/
assets/
data.json
templates/
welcome.html
server/
routes/
index.ts
```
```ts [server/routes/index.ts]
import { useStorage } from "nitro/storage";
export default defineHandler(async () => {
const serverAssets = useStorage("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:
```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 { useStorage } from "nitro/storage";
const templates = useStorage("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 { useStorage } from "nitro/storage";
const serverAssets = useStorage("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, server assets are read directly from the filesystem. In production, they are bundled and inlined into the build output.
::
## 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 { useStorage } from "nitro/storage";
import { definePlugin } from "nitro";
import redisDriver from "unstorage/drivers/redis";
export default definePlugin(() => {
const storage = useStorage()
// Dynamically pass in credentials from runtime configuration, or other sources
const driver = redisDriver({
base: "redis",
host: process.env.REDIS_HOST,
port: process.env.REDIS_PORT,
/* other redis connector options */
})
// Mount driver
storage.mount("redis", driver)
})
```
::warning
This is a temporary workaround, with a better solution coming in the future! Keep a lookout on the GitHub issue [here](https://github.com/nitrojs/nitro/issues/1161#issuecomment-1511444675).
::
---
# Assets
Nitro supports two types of assets: **public assets** served directly to clients and **server assets** bundled into the server for programmatic access.
## Public Assets
Nitro handles assets via the `public/` directory.
All assets in `public/` directory will be automatically served. This means that you can access them directly from the browser without any special configuration.
```md
public/
image.png <-- /image.png
video.mp4 <-- /video.mp4
robots.txt <-- /robots.txt
```
### Caching and Headers
Public assets are served with automatic `ETag` and `Last-Modified` headers for conditional requests. When the client sends `If-None-Match` or `If-Modified-Since` headers, Nitro returns a `304 Not Modified` response.
For assets served from a non-root `baseURL` (such as `/build/`), Nitro prevents fallthrough to application handlers. If a request matches a public asset base but the file is not found, a `404` is returned immediately.
### Production Public Assets
When building your Nitro app, the `public/` directory will be copied to `.output/public/` and a manifest with metadata will be created and embedded in the server bundle.
```json
{
"/image.png": {
"type": "image/png",
"etag": "\"4a0c-6utWq0Kbk5OqDmksYCa9XV8irnM\"",
"mtime": "2023-03-04T21:39:45.086Z",
"size": 18956
},
"/robots.txt": {
"type": "text/plain; charset=utf-8",
"etag": "\"8-hMqyDrA8fJ0R904zgEPs3L55Jls\"",
"mtime": "2023-03-04T21:39:45.086Z",
"size": 8
},
"/video.mp4": {
"type": "video/mp4",
"etag": "\"9b943-4UwfQXKUjPCesGPr6J5j7GzNYGU\"",
"mtime": "2023-03-04T21:39:45.085Z",
"size": 637251
}
}
```
This allows Nitro to know the public assets without scanning the directory, giving high performance with caching headers.
### Custom Public Asset Directories
You can configure additional public asset directories using the `publicAssets` config option. Each entry supports the following properties:
- `dir` -- Path to the directory (resolved relative to `rootDir`).
- `baseURL` -- URL prefix for serving assets (default: `"/"`).
- `maxAge` -- Cache `max-age` in seconds. When set, a `Cache-Control: public, max-age=, immutable` header is applied via route rules.
- `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.
## Server Assets
All assets in `assets/` directory 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]
> Unless using `useStorage()`, 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";
export default defineHandler(async () => {
const data = await useStorage("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";
export default defineHandler(async (event) => {
const html = await useStorage("assets:templates").get("success.html");
return html;
});
```
> [!TIP]
> 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).
## 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.
---
# Configuration
> Customize and extend Nitro defaults.
::read-more{to="/config"}
See [config reference](/config) for available options.
::
## Config file
You can customize your Nitro builder with a configuration file.
```ts [nitro.config.ts]
import { defineConfig } from "nitro";
export default defineConfig({
// Nitro options
})
```
```ts [vite.config.ts]
import { defineConfig } from 'vite'
import { nitro } from 'nitro/vite'
export default defineConfig({
plugins: [
nitro()
],
nitro: {
// Nitro options
}
})
```
> [!TIP]
> Nitro loads the configuration using [c12](https://github.com/unjs/c12), giving more possibilities such as using `.nitrorc` file in current working directory or in the user's home directory.
### Environment-specific config
Using [c12](https://github.com/unjs/c12) conventions, you can provide environment-specific overrides using `$development` and `$production` keys:
```ts [nitro.config.ts]
import { defineConfig } from "nitro";
export default defineConfig({
logLevel: 3,
$development: {
// Options applied only in development mode
debug: true,
},
$production: {
// Options applied only in production builds
minify: true,
},
})
```
The environment name is `"development"` during `nitro dev` and `"production"` during `nitro build`.
### Extending configs
You can extend from other configs or presets using the `extends` key:
```ts [nitro.config.ts]
import { defineConfig } from "nitro";
export default defineConfig({
extends: "./base.config",
})
```
### Config from `package.json`
You can also provide Nitro configuration under the `nitro` key in your `package.json` file.
## 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.
## Environment variables
Certain Nitro behaviors can be configured using environment variables:
| Variable | Description |
| --- | --- |
| `NITRO_PRESET` | Override the deployment preset. |
| `NITRO_COMPATIBILITY_DATE` | Set the compatibility date. |
| `NITRO_APP_BASE_URL` | Override the base URL (default: `/`). |
## Runtime configuration
Nitro provides a runtime config API to expose configuration within your application, with the ability to update it at runtime by setting environment variables. This is useful when you want to expose different configuration values for different environments (e.g. development, staging, production). For example, you can use this to expose different API endpoints for different environments or to expose different feature flags.
First, you need to 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 in development and use platform variables in production (see below).
Create an `.env` file in your project root:
```bash [.env]
NITRO_API_TOKEN="123"
```
Re-start 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 only loaded during development (`nitro dev`). In production, use your platform's native environment variable mechanism.
Do not forget that you can still universally access environment variables using `import.meta.env` or `process.env` but avoid using them in ambient global contexts 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 [.env]
NITRO_API_TOKEN="123"
```
In runtime config, define key using camelCase. In environment variables, define key using snake_case and uppercase.
```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.
### 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.
In order to enable database layer you need to enable experimental feature flag.
```ts [nitro.config.ts]
import { defineConfig } from "nitro";
export default defineConfig({
experimental: {
database: true
}
})
```
> [!TIP]
> You can change default connection or define more connections to any of the [supported databases](https://db0.unjs.io/connectors/sqlite).
> [!TIP]
> You can integrate database instance to any of the [supported ORMs](https://db0.unjs.io/integrations).
## 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");
```
> [!NOTE]
> When `experimental.database` is enabled, `useDatabase` is auto-imported and available without an explicit import statement.
Database instances are created lazily on first use and cached for subsequent calls with the same connection name. If a connection name is not configured, an error will be thrown.
### `db.sql`
Execute SQL queries using tagged template literals with automatic parameter binding:
```ts
const db = useDatabase();
// Insert with parameterized values (safe from SQL injection)
const id = "1001";
await db.sql`INSERT INTO users VALUES (${id}, 'John', 'Doe', 'john@example.com')`;
// Query with parameters
const { rows } = await db.sql`SELECT * FROM users WHERE id = ${id}`;
// The result includes rows, changes count, and last insert ID
const result = await db.sql`INSERT INTO posts (title) VALUES (${"Hello"})`;
// result.rows, result.changes, result.lastInsertRowid
```
### `db.exec`
Execute a raw SQL string directly:
```ts
const db = useDatabase();
await db.exec("CREATE TABLE IF NOT EXISTS users (id TEXT PRIMARY KEY, name TEXT)");
```
### `db.prepare`
Prepare an SQL statement for repeated execution:
```ts
const db = useDatabase();
const stmt = db.prepare("SELECT * FROM users WHERE id = ?");
const result = await stmt.bind("1001").all();
```
## Configuration
You can configure database connections using `database` config.
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" }
}
}
});
```
> [!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 |
| `postgresql` | [PostgreSQL](https://github.com/porsager/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 |
| `cloudflare-d1` | [Cloudflare D1](https://developers.cloudflare.com/d1/) |
| `cloudflare-hyperdrive-mysql` | Cloudflare Hyperdrive with MySQL |
| `cloudflare-hyperdrive-postgresql` | Cloudflare Hyperdrive with PostgreSQL |
---
# Lifecycle
> Understand how Nitro runs and serves incoming requests to your application.
## 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 [server 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.url.pathname}`);
});
});
```
::note
Errors thrown inside the `request` hook are captured by the [`error` hook](#error-handling) and do not terminate the request pipeline.
::
### Static assets
When static asset serving is enabled (the default for most presets), Nitro checks if the request matches a file in the `public/` directory **before** any other middleware or route handler runs.
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.
### Route rules
The matching route rules defined in the Nitro config will execute. Route rules run as middleware so most of them alter the response without terminating it (for instance, adding a header or setting a cache policy).
```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"}
### Global middleware
Any global middleware defined in the `middleware/` directory will be run:
```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 that targets a specific route pattern (defined with a `route` in `middleware/`) runs after global middleware but before the matched route handler.
### Routes
Nitro will look at defined routes in the `routes/` folder to match the incoming request.
```ts [routes/api/hello.ts]
export default (event) => ({ world: true })
```
::read-more{to="/docs/routing#filesystem-routing"}
Learn more about Nitro file-system routing.
::
If serverEntry is defined it will catch all requests not matching any other route acting as `/**` route handler.
```ts [server.ts]
import { defineHandler } from "nitro";
export default defineHandler((event) => {
if (event.path === "/") {
return "Home page";
}
});
```
::read-more{to="/docs/server-entry"}
Learn more about 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.url.pathname}`);
});
});
```
::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.
```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"
});
});
```
Errors are also tracked per-request in `event.req.context.nitro.errors` for inspection in later hooks.
You can provide a custom error handler in the Nitro config to control error response formatting:
```ts [nitro.config.ts]
import { defineConfig } from "nitro";
export default defineConfig({
errorHandler: "~/error",
})
```
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
});
});
```
## Hooks reference
All runtime hooks are registered through [server 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 plugins to extend Nitro's runtime behavior.
Nitro plugins are **executed once** during server startup in order to allow extending Nitro's runtime behavior.
They receive `nitroApp` context, which can be used to hook into lifecycle events.
Plugins are auto-registered from the `plugins/` directory and run synchronously by file name order on the first Nitro initialization. Plugin functions themselves must be synchronous (return `void`), but the hooks they register can be async.
**Example:**
```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']
})
```
## 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. |
## Nitro runtime hooks
You can use Nitro [hooks](https://github.com/unjs/hookable) to extend the default runtime behaviour of Nitro by registering custom functions to the lifecycle events within plugins.
**Example:**
```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
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
import { definePlugin } from "nitro";
export default definePlugin((nitroApp) => {
nitroApp.hooks.hook("error", async (error, { event }) => {
console.error(`${event?.path} 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
import { definePlugin } from "nitro";
export default definePlugin((nitroApp) => {
nitroApp.captureError(new Error("something went wrong"), {
tags: ["startup"],
});
});
```
### Graceful shutdown
Server will gracefully shutdown and wait for any background pending tasks initiated by `event.waitUntil`.
```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:
```ts
import { definePlugin } from "nitro";
export default definePlugin((nitroApp) => {
nitroApp.hooks.hook("request", (event) => {
console.log("on request", event.path);
});
nitroApp.hooks.hook("response", (res, event) => {
// Modify or inspect the response
console.log("on response", res.status);
});
});
```
### Modifying response 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");
}
});
});
```
---
# Tasks
> Nitro tasks allow on-off operations in runtime.
## 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 experimental feature flag.
```ts [nitro.config.ts]
import { defineConfig } from "nitro";
export default defineConfig({
experimental: {
tasks: true
}
})
```
## Define tasks
Tasks can be defined in `tasks/[name].ts` files.
Nested directories are supported. The task name will be joined with `:`. (Example: `tasks/db/migrate.ts` task name will be `db:migrate`)
**Example:**
```ts [tasks/db/migrate.ts]
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