# Introduction
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/){rel=""nofollow""}, [h3](https://h3.dev){rel=""nofollow""}, or [Hono](https://hono.dev){rel=""nofollow""}.
### 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){rel=""nofollow""} 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](https://nitro.build/docs/quick-start).
# Quick Start
## 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){rel=""nofollow""}
## 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){rel=""nofollow""}, [Bun](https://bun.sh/){rel=""nofollow""}, or [Deno](https://deno.com/){rel=""nofollow""}.
::
:pm-x{command="create-nitro-app"}
Preview
::div{style="display:flex;justify-content:center;"}
{style="max-width:100%;height:auto;display:block;"}
::
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 { defineNitroConfig } from "nitro/config";
export default defineNitroConfig({
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{default-value="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 { defineNitroConfig } from "nitro/config";
export default defineNitroConfig({
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](https://nitro.build/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](https://nitro.build/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` ✨
::
# Nitro Renderer
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 { defineNitroConfig } from "nitro/config";
export default defineNitroConfig({
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 { defineNitroConfig } from "nitro/config";
export default defineNitroConfig({
renderer: {
template: './app.html'
}
})
```
```html [app.html]
Custom Template
Loading...
```
::
### Static templates
By default, Nitro auto-detects whether your HTML template contains [rendu](https://nitro.build/#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 { defineNitroConfig } from "nitro/config";
export default defineNitroConfig({
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){rel=""nofollow""} 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{title="Rendu Documentation" to="https://github.com/h3js/rendu"}
## 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 { defineNitroConfig } from "nitro/config";
export default defineNitroConfig({
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:
::steps{level="4"}
#### Specific API routes are matched first (e.g., `/api/users`)
#### Specific server routes are matched next (e.g., `/about`)
#### 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{title="Lifecycle" to="https://nitro.build/docs/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
## Request handler
Nitro request handler is a function accepting an `event` object, which is a [H3Event](https://h3.dev/guide/api/h3event#h3event-properties){rel=""nofollow""} 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){rel=""nofollow""}). 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](https://nitro.build/#specific-request-method) to the filename to define a specific request method.
```text
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){rel=""nofollow""} 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`](https://nitro.build/#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 { defineNitroConfig } from "nitro/config";
export default defineNitroConfig({
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 { defineNitroConfig } from "nitro/config";
export default defineNitroConfig({
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 { defineNitroConfig } from "nitro/config";
export default defineNitroConfig({
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){rel=""nofollow""}.
::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`](https://nitro.build/#handlers-config) config with the `middleware` option and a specific `route`:
```ts [nitro.config.ts]
import { defineNitroConfig } from "nitro/config";
export default defineNitroConfig({
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){rel=""nofollow""} 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`](https://nitro.build/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){rel=""nofollow""}) to route options.
When `cache` option is set, handlers matching pattern will be automatically wrapped with `defineCachedEventHandler`. See the [cache guide](https://nitro.build/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 { defineNitroConfig } from "nitro/config";
export default defineNitroConfig({
routeRules: {
'/blog/**': { swr: true },
'/blog/**': { swr: 600 },
'/blog/**': { static: true },
'/blog/**': { 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 { defineNitroConfig } from "nitro/config";
export default defineNitroConfig({
routeRules: {
'/api/cached/**': { swr: true },
'/api/cached/no-cache': { cache: false, swr: false },
'/admin/**': { basicAuth: { username: 'admin', password: 'secret' } },
'/admin/public/**': { basicAuth: false },
}
});
```
### Headers
Set custom response headers for matching routes:
```ts [nitro.config.ts]
import { defineNitroConfig } from "nitro/config";
export default defineNitroConfig({
routeRules: {
'/api/**': { headers: { 'cache-control': 's-maxage=60' } },
'**': { headers: { 'x-powered-by': 'Nitro' } },
}
});
```
### CORS
Enable CORS headers with the `cors: true` shortcut. This sets `access-control-allow-origin: *`, `access-control-allow-methods: *`, `access-control-allow-headers: *`, and `access-control-max-age: 0`.
You can override individual CORS headers using `headers`:
```ts [nitro.config.ts]
import { defineNitroConfig } from "nitro/config";
export default defineNitroConfig({
routeRules: {
'/api/v1/**': {
cors: true,
headers: { 'access-control-allow-methods': 'GET' },
},
}
});
```
### 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 { defineNitroConfig } from "nitro/config";
export default defineNitroConfig({
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 { defineNitroConfig } from "nitro/config";
export default defineNitroConfig({
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 { defineNitroConfig } from "nitro/config";
export default defineNitroConfig({
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 { defineNitroConfig } from "nitro/config";
export default defineNitroConfig({
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 { defineNitroConfig } from "nitro/config";
export default defineNitroConfig({
routeRules: {
'/about': { prerender: true },
'/dynamic/**': { prerender: false },
}
});
```
### ISR (Vercel)
Configure Incremental Static Regeneration for Vercel deployments:
```ts [nitro.config.ts]
import { defineNitroConfig } from "nitro/config";
export default defineNitroConfig({
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` | Enable permissive CORS headers |
| `cache` | `object | false` | Cache options (see [cache guide](https://nitro.build/docs/cache)) |
| `swr` | `boolean | number` | Shortcut for `cache: { swr: true, maxAge: number }` |
| `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 { defineNitroConfig } from "nitro/config";
export default defineNitroConfig({
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 |
# 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 { defineNitroConfig } from "nitro/config";
export default defineNitroConfig({
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 { defineNitroConfig } from "nitro/config";
export default defineNitroConfig({
compressPublicAssets: true,
});
```
Or pick specific encodings:
```js [nitro.config.ts]
import { defineNitroConfig } from "nitro/config";
export default defineNitroConfig({
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](https://nitro.build/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 { defineNitroConfig } from "nitro/config";
export default defineNitroConfig({
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).
::
# Configuration
::read-more{to="https://nitro.build/config"}
See [config reference](https://nitro.build/config) for available options.
::
## Config file
You can customize your Nitro builder with a configuration file.
::CodeGroup
```ts [nitro.config.ts]
import { defineNitroConfig } from "nitro/config";
export default defineNitroConfig({
// 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){rel=""nofollow""}, 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){rel=""nofollow""} conventions, you can provide environment-specific overrides using `$development` and `$production` keys:
```ts [nitro.config.ts]
import { defineNitroConfig } from "nitro/config";
export default defineNitroConfig({
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 { defineNitroConfig } from "nitro/config";
export default defineNitroConfig({
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 { defineNitroConfig } from "nitro/config";
export default defineNitroConfig({
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 { defineNitroConfig } from "nitro/config";
export default defineNitroConfig({
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:
::CodeGroup
```ts [nitro.config.ts]
import { defineNitroConfig } from "nitro/config";
export default defineNitroConfig({
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 { defineNitroConfig } from "nitro/config";
export default defineNitroConfig({
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 { defineNitroConfig } from "nitro/config";
export default defineNitroConfig({
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
The default database connection is **preconfigured** with [SQLite](https://db0.unjs.io/connectors/sqlite){rel=""nofollow""} 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{title="DB0 Documentation" to="https://db0.unjs.io"}
::important
Database support is currently experimental.
Refer to the [db0 issues](https://github.com/unjs/db0/issues){rel=""nofollow""} for status and bug report.
::
In order to enable database layer you need to enable experimental feature flag.
```ts [nitro.config.ts]
import { defineNitroConfig } from "nitro/config";
export default defineNitroConfig({
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){rel=""nofollow""}.
::
::tip
You can integrate database instance to any of the [supported ORMs](https://db0.unjs.io/integrations){rel=""nofollow""}.
::
## 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:
```ts [nitro.config.ts]
import { defineNitroConfig } from "nitro/config";
export default defineNitroConfig({
database: {
default: {
connector: "sqlite",
options: { name: "db" }
},
users: {
connector: "postgresql",
options: {
url: "postgresql://username:password@hostname:port/database_name"
},
},
},
});
```
### 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 { defineNitroConfig } from "nitro/config";
export default defineNitroConfig({
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){rel=""nofollow""}. 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){rel=""nofollow""} |
| `sqlite3` | [sqlite3](https://github.com/TryGhost/node-sqlite3){rel=""nofollow""} |
| `bun` / `bun-sqlite` | Bun built-in SQLite |
| `libsql` / `libsql-node` | [libSQL](https://github.com/tursodatabase/libsql){rel=""nofollow""} (Node.js) |
| `libsql-http` | libSQL over HTTP |
| `libsql-web` | libSQL for web environments |
| `postgresql` | [PostgreSQL](https://github.com/porsager/postgres){rel=""nofollow""} |
| `mysql2` | [MySQL](https://github.com/sidorares/node-mysql2){rel=""nofollow""} |
| `pglite` | [PGlite](https://github.com/electric-sql/pglite){rel=""nofollow""} (embedded PostgreSQL) |
| `planetscale` | [PlanetScale](https://github.com/planetscale/database-js){rel=""nofollow""} serverless |
| `cloudflare-d1` | [Cloudflare D1](https://developers.cloudflare.com/d1/){rel=""nofollow""} |
| `cloudflare-hyperdrive-mysql` | Cloudflare Hyperdrive with MySQL |
| `cloudflare-hyperdrive-postgresql` | Cloudflare Hyperdrive with PostgreSQL |
# Lifecycle
## 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](https://nitro.build/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.path}`);
});
});
```
:::note
Errors thrown inside the `request` hook are captured by the [`error` hook](https://nitro.build/#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 { defineNitroConfig } from "nitro/config";
export default defineNitroConfig({
routeRules: {
'/**': { headers: { 'x-nitro': 'first' } }
}
})
```
:read-more{title="Routing > Route rules" to="https://nitro.build/docs/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="https://nitro.build/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="https://nitro.build/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="https://nitro.build/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="https://nitro.build/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.path}`);
});
});
```
:::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:
::steps{level="4"}
#### Calls the `error` hook with the error and context (including the event and source tags).
#### 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 { defineNitroConfig } from "nitro/config";
export default defineNitroConfig({
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](https://nitro.build/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="https://nitro.build/docs/plugins"}
Learn more about Nitro plugins and hook usage examples.
::
# Plugins
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 { defineNitroConfig } from "nitro/config";
export default defineNitroConfig({
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){rel=""nofollow""} | Hook system for registering lifecycle callbacks. |
| `h3` | `H3Core` | The underlying [H3](https://github.com/h3js/h3){rel=""nofollow""} 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){rel=""nofollow""} 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
## Opt-in to the experimental feature
::important
Tasks support is currently experimental.
See [nitrojs/nitro#1974](https://github.com/nitrojs/nitro/issues/1974){rel=""nofollow""} for the relevant discussion.
::
In order to use the tasks API you need to enable experimental feature flag.
```ts [nitro.config.ts]
import { defineNitroConfig } from "nitro/config";
export default defineNitroConfig({
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`](https://nitro.build/#taskevent) and returns (or resolves to) an object with an optional `result` property.
```ts
interface Task