# 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;"} ![Preview](https://github.com/nitrojs/create-nitro-app/blob/main/.images/preview.png?raw=true){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 `` — raw (unescaped) output ```html

Hello {{ $URL.pathname }}

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

Form submitted!

``` #### Server scripts Use `
{{ JSON.stringify(data) }}
``` #### Streaming content Use the `echo()` function for streaming content. It accepts strings, functions, Promises, Response objects, or ReadableStreams: ```html ``` #### Global variables Access request context within templates: | Variable | Description | | ----------- | ------------------------------------------- | | `$REQUEST` | The incoming `Request` object | | `$METHOD` | HTTP method (`GET`, `POST`, etc.) | | `$URL` | Request `URL` object | | `$HEADERS` | Request headers | | `$RESPONSE` | Response configuration object | | `$COOKIES` | Read-only object containing request cookies | #### Built-in functions | Function | Description | | ---------------------------------- | ---------------------------------------------------------------- | | `htmlspecialchars(str)` | Escape HTML characters (automatically applied in `{{ }}` syntax) | | `setCookie(name, value, options?)` | Set a cookie in the response | | `redirect(url)` | Redirect the user to another URL | | `echo(content)` | Stream content to the response | ```html [index.html] Dynamic template

Hello {{ $REQUEST.url }}

Welcome, !

``` :read-more{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 { meta?: { name?: string; description?: string }; run(event: TaskEvent): { result?: RT } | Promise<{ result?: RT }>; } ``` ### `TaskEvent` The `run` function receives a `TaskEvent` object with the following properties: - **`name`**: The name of the task being executed. - **`payload`**: An object (`Record`) containing any data passed to the task. - **`context`**: A `TaskContext` object (may include `waitUntil` depending on the runtime). ```ts interface TaskEvent { name: string; payload: TaskPayload; context: TaskContext; } ``` ### Registering tasks via config In addition to file-based scanning, tasks can be registered directly in the Nitro config. This is useful for tasks provided by modules or pointing to custom handler paths. ```ts [nitro.config.ts] import { defineNitroConfig } from "nitro/config"; export default defineNitroConfig({ experimental: { tasks: true }, tasks: { "db:migrate": { handler: "./tasks/custom-migrate.ts", description: "Run database migrations" } } }) ``` If a task is both scanned from the `tasks/` directory and defined in the config, the config-defined `handler` takes precedence. ## Scheduled tasks You can define scheduled tasks using Nitro configuration to automatically run after each period of time. ```ts [nitro.config.ts] import { defineNitroConfig } from "nitro/config"; export default defineNitroConfig({ scheduledTasks: { // Run `cms:update` task every minute '* * * * *': ['cms:update'], // Run a single task (string shorthand) '0 * * * *': 'db:cleanup' } }) ``` The `scheduledTasks` config maps cron expressions to either a single task name (string) or an array of task names. When multiple tasks are assigned to the same cron expression, they run in parallel. ::tip You can use [crontab.guru](https://crontab.guru/){rel=""nofollow""} to easily generate and understand cron tab patterns. :: When a scheduled task runs, it automatically receives a `payload` with `scheduledTime` set to the current timestamp (`Date.now()`). ### Platform support - **`dev`**, **`node_server`**, **`node_cluster`**, **`node_middleware`**, **`bun`** and **`deno_server`** presets are supported with the [croner](https://croner.56k.guru/){rel=""nofollow""} engine. - **`cloudflare_module`** and **`cloudflare_pages`** presets have native integration with [Cron Triggers](https://developers.cloudflare.com/workers/configuration/cron-triggers/){rel=""nofollow""}. Nitro automatically generates the cron triggers in the wrangler config at build time - no manual wrangler setup required. - **`vercel`** preset has native integration with [Vercel Cron Jobs](https://vercel.com/docs/cron-jobs){rel=""nofollow""}. Nitro automatically generates the cron job configuration at build time - no manual `vercel.json` setup required. You can secure cron endpoints by setting the `CRON_SECRET` environment variable. - More presets (with native primitives support) are planned to be supported! ## `waitUntil` When running background tasks, you might want to make sure the server or worker waits until the task is done. An optional `context.waitUntil` function *might* be available depending on the runtime. ```ts export default defineTask({ run({ context }) { const promise = fetch(...) context.waitUntil?.(promise); await promise; return { result: "Success" }; }, }); ``` ## Programmatically run tasks To manually run tasks, you can use `runTask(name, { payload?, context? })` utility from `nitro/task`. **Example:** ```ts [api/migrate.ts] import { defineHandler } from "nitro"; export default defineHandler(async (event) => { // IMPORTANT: Authenticate user and validate payload! const payload = Object.fromEntries(event.url.searchParams); const { result } = await runTask("db:migrate", { payload }); return { result }; }); ``` ### Error handling `runTask` throws an HTTP error if: - The task does not exist (status `404`). - The task has no handler implementation (status `501`). Any errors thrown inside the task's `run` function will propagate to the caller. ## Run tasks with dev server Nitro's built-in dev server exposes tasks to be easily executed without programmatic usage. ### Using API routes #### `/_nitro/tasks` This endpoint returns a list of available task names and their meta. ```json // [GET] /_nitro/tasks { "tasks": { "db:migrate": { "description": "Run database migrations" }, "cms:update": { "description": "Update CMS content" } }, "scheduledTasks": [ { "cron": "* * * * *", "tasks": [ "cms:update" ] } ] } ``` #### `/_nitro/tasks/:name` This endpoint executes a task. You can provide a payload using both query parameters and body JSON payload. The payload sent in the JSON body payload must be under the `"payload"` property. ::code-group ```ts [tasks/echo/payload.ts] export default defineTask({ meta: { name: "echo:payload", description: "Returns the provided payload", }, run({ payload, context }) { console.log("Running echo task..."); return { result: payload }; }, }); ``` ```json [GET] // [GET] /_nitro/tasks/echo:payload?field=value&array=1&array=2 { "field": "value", "array": ["1", "2"] } ``` ```json [POST] /** * [POST] /_nitro/tasks/echo:payload?field=value * body: { * "payload": { * "answer": 42, * "nested": { * "value": true * } * } * } */ { "field": "value", "answer": 42, "nested": { "value": true } } ``` :: ::note The JSON payload included in the body will overwrite the keys present in the query params. :: ### Using CLI ::important It is only possible to run these commands while the **dev server is running**. You should run them in a second terminal. :: #### List tasks ```sh nitro task list ``` #### Run a task ```sh nitro task run db:migrate --payload "{}" ``` The `--payload` flag accepts a JSON string that will be parsed and passed to the task. If the value is not a valid JSON object, the task runs without a payload. ## Notes ### Concurrency Each task can have **one running instance**. Calling a task of same name multiple times in parallel, results in calling it once and all callers will get the same return value. ::note Nitro tasks can be running multiple times and in parallel. :: # Nitro Server Entry 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{icon="i-undocs-h3" label="H3"} ```ts [server.ts] import { H3 } from "h3"; const app = new H3() app.get("/", () => "⚡️ Hello from H3!"); export default app; ``` ::: :::tabs-item{icon="i-undocs-hono" label="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{icon="i-undocs-elysia" label="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/){rel=""nofollow""} or [Fastify](https://fastify.dev/){rel=""nofollow""}), 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/){rel=""nofollow""}. ::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 { defineNitroConfig } from "nitro/config"; export default defineNitroConfig({ serverEntry: "./nitro.server.ts" }) ``` You can also provide an object with `handler` and `format` options: ```ts [nitro.config.ts] import { defineNitroConfig } from "nitro/config"; export default defineNitroConfig({ 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 { defineNitroConfig } from "nitro/config"; export default defineNitroConfig({ 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](https://nitro.build/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 ## 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](https://nitro.build/#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](https://nitro.build/#options) section for more details about the available options. ::important **Request headers are dropped** when handling cached responses. Use the [`varies` option](https://nitro.build/#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... :: ::callout If you are using edge workers to host your application, you should follow the instructions below. :::collapsible{name="Edge workers instructions"} 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 { defineNitroConfig } from "nitro/config"; export default defineNitroConfig({ routeRules: { "/blog/**": { cache: { maxAge: 60 * 60 } }, }, }); ``` If we want to use a [custom cache storage](https://nitro.build/#cache-storage) mount point, we can use the `base` option. ```ts [nitro.config.ts] import { defineNitroConfig } from "nitro/config"; export default defineNitroConfig({ 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 { defineNitroConfig } from "nitro/config"; export default defineNitroConfig({ routeRules: { "/blog/**": { swr: true }, "/api/**": { swr: 3600 }, }, }); ``` To explicitly disable caching on a route, set `cache: false`: ```ts [nitro.config.ts] import { defineNitroConfig } from "nitro/config"; export default defineNitroConfig({ 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){rel=""nofollow""} by default. - In development, it will use the [filesystem driver](https://unstorage.unjs.io/drivers/fs){rel=""nofollow""}, 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 { defineNitroConfig } from "nitro/config"; export default defineNitroConfig({ 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 { defineNitroConfig } from "nitro/config"; export default defineNitroConfig({ 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: ::steps{level="4"} #### The stale cached value is returned immediately to the client. #### The function/handler is called in the background to refresh the cache. #### On edge workers, `event.waitUntil` is used to keep the background refresh alive. :: When `swr` is disabled and a cached value has expired: ::steps{level="4"} #### The stale entry is cleared. #### The client waits for the function/handler to resolve with a fresh value. :: ## Cache keys and invalidation 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 ``` You can invalidate the cached function entry with: ```ts import { useStorage } from "nitro/storage"; await useStorage('cache').removeItem('nitro/functions:getAccessToken:default.json') ``` ::note For cached handlers, the cache key includes a hash of the URL path and, when using the [`varies`](https://nitro.build/#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="https://nitro.build/docs/storage"} Read more about the Nitro storage. :: # KV Storage Nitro has built-in integration with [unstorage](https://unstorage.unjs.io){rel=""nofollow""} 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 { defineNitroConfig } from "nitro/config"; export default defineNitroConfig({ 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/){rel=""nofollow""} 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 { defineNitroConfig } from "nitro/config"; export default defineNitroConfig({ 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](https://nitro.build/#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. ```text 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 { defineNitroConfig } from "nitro/config"; export default defineNitroConfig({ 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](https://nitro.build/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){rel=""nofollow""}. :: # Migration Guide ::note This is a living document for migrating from Nitro 2 to 3. Please check it regularly while using the beta version. :: Nitro v3 introduces intentional backward-incompatible changes. This guide helps you migrate from Nitro v2. ## `nitropack` is renamed to `nitro` The NPM package [nitropack](https://www.npmjs.com/package/nitropack){rel=""nofollow""} (v2) has been renamed to [nitro](https://www.npmjs.com/package/nitro){rel=""nofollow""} (v3). **Migration:** Update the `nitropack` dependency to `nitro` in `package.json`: ::CodeGroup ```diff [release channel] { "dependencies": { -- "nitropack": "latest" ++ "nitro": "latest" } } ``` ```diff [nightly channel] { "dependencies": { -- "nitropack": "latest" ++ "nitro": "npm:nitro-nightly" } } ``` :: **Migration:** Search your codebase and rename all instances of nitropack to nitro: ```diff -- import { defineNitroConfig } from "nitropack/config" ++ import { defineNitroConfig } from "nitro/config" ``` ## nitro/runtime Runtime utils had been moved to individual `nitro/*` subpath exports. Refer to docs for usage. ```diff -- import { useStorage } from "nitropack/runtime/storage" ++ import { useStorage } from "nitro/storage" ``` ## Minimum Supported Node.js Version: 20 Nitro now requires a minimum Node.js version of 20, as Node.js 18 reaches end-of-life in [April 2025](https://nodejs.org/en/about/previous-releases){rel=""nofollow""}. Please upgrade to the [latest LTS](https://nodejs.org/en/download){rel=""nofollow""} version (>= 20). **Migration:** - Check your local Node.js version using `node --version` and update if necessary. - If you use a CI/CD system for deployment, ensure that your pipeline is running Node.js 20 or higher. - If your hosting provider manages the Node.js runtime, make sure it's set to version 20, 22, or later. ## Type Imports Nitro types are now only exported from `nitro/types`. **Migration:** Import types from nitro/types instead of nitro: ```diff -- import { NitroRuntimeConfig } from "nitropack" ++ import { NitroRuntimeConfig } from "nitro/types" ``` ## App Config Support Removed Nitro v2 supported a bundled app config that allowed defining configurations in `app.config.ts` and accessing them at runtime via `useAppConfig()`. This feature had been removed. **Migration:** Use a regular `.ts` file in your server directory and import it directly. ## Preset updates Nitro presets have been updated for the latest compatibility. Some (legacy) presets have been removed or renamed. | Old Preset | New Preset | | ------------------------------------------------------------- | -------------------------------------------------- | | `node` | `node_middleware` (export changed to `middleware`) | | `cloudflare`, `cloudflare_worker`, `cloudflare_module_legacy` | `cloudflare_module` | | `deno-server-legacy` | `deno_server` with Deno v2 | | `netlify-builder` | `netlify` or `netlify_edge` | | `vercel-edge` | `vercel` with Fluid compute enabled | | `azure`, `azure_functions` | `azure_swa` | | `firebase` | `firebase_app_hosting` | | `iis` | `iis_handler` | | `deno` | `deno_deploy` | | `edgio` | Discontinued | | `cli` | Removed due to lack of use | | `service_worker` | Removed due to instability | ## Cloudflare Bindings Access In Nitro v2, Cloudflare environment variables and bindings were accessible via `event.context.cloudflare.env`. In Nitro v3, the Cloudflare runtime context is attached to the request's runtime object instead. **Migration:** ```diff -- const { cloudflare } = event.context -- const binding = cloudflare.env.MY_BINDING ++ const { env } = event.req.runtime.cloudflare ++ const binding = env.MY_BINDING ``` ## Changed nitro subpath imports Nitro v2 introduced multiple subpath exports, some of which have been removed or updated: - `nitro/rollup`, `nitropack/core` (use `nitro/builder`) - `nitropack/runtime/*` (use `nitro/*`) - `nitropack/kit` (removed) - `nitropack/presets` (removed) An experimental `nitropack/kit` was introduced but has now been removed. A standalone Nitro Kit package may be introduced in the future with clearer objectives. **Migration:** - Use `NitroModule` from `nitro/types` instead of `defineNitroModule` from the kit. - Prefer built-in Nitro presets (external presets are only for evaluation purposes). ## H3 v2 Nitro v3 upgrades to [H3 v2](https://h3.dev){rel=""nofollow""}, which includes API changes. All H3 utilities are imported from `nitro/h3`. ### Web Standards H3 v2 is rewritten based on web standard primitives ([`URL`](https://developer.mozilla.org/en-US/docs/Web/API/URL){rel=""nofollow""}, [`Headers`](https://developer.mozilla.org/en-US/docs/Web/API/Headers){rel=""nofollow""}, [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request){rel=""nofollow""}, and [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response){rel=""nofollow""}). Access to `event.node.{req,res}` is only available in Node.js runtime. `event.web` is renamed to `event.req` (instance of web [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request){rel=""nofollow""}). ### Response Handling You should always explicitly **return** the response body or **throw** an error: ```diff -- import { send, sendRedirect, sendStream } from "nitro/h3" -- send(event, value) -- sendStream(event, stream) -- sendRedirect(event, location, code) ++ import { redirect } from "nitro/h3" ++ return value ++ return stream ++ return redirect(event, location, code) ``` Other changes: - `sendError(event, error)` → `throw createError(error)` - `sendNoContent(event)` → `return noContent(event)` - `sendProxy(event, target)` → `return proxy(event, target)` ### Request Body Most body utilities can be replaced with native `event.req` methods: ```diff -- import { readBody, readRawBody, readFormData } from "nitro/h3" ++ // Use native Request methods ++ const json = await event.req.json() ++ const text = await event.req.text() ++ const formData = await event.req.formData() ++ const stream = event.req.body ``` ### Headers H3 now uses standard web [`Headers`](https://developer.mozilla.org/en-US/docs/Web/API/Headers){rel=""nofollow""}. Header values are always plain `string` (no `null`, `undefined`, or `string[]`). ```diff -- import { getHeader, setHeader, getResponseStatus } from "nitro/h3" -- getHeader(event, "x-foo") -- setHeader(event, "x-foo", "bar") ++ event.req.headers.get("x-foo") ++ event.res.headers.set("x-foo", "bar") ++ event.res.status // instead of getResponseStatus(event) ``` ### Handler Utils ```diff -- import { eventHandler, defineEventHandler } from "nitro/h3" ++ import { defineHandler } from "nitro" ``` - `lazyEventHandler` → `defineLazyEventHandler` - `useBase` → `withBase` ### Error Utils ```diff -- import { createError, isError } from "nitro/h3" ++ import { HTTPError } from "nitro" ++ throw new HTTPError({ status: 404, message: "Not found" }) ++ HTTPError.isError(error) ``` ### Node.js Utils ```diff -- import { defineNodeListener, fromNodeMiddleware, toNodeListener } from "nitro/h3" ++ import { defineNodeHandler, fromNodeHandler, toNodeHandler } from "nitro/h3" ``` ## Optional Hooks If you were using `useNitroApp().hooks` outside of Nitro plugins before, it might be undefined. Use new `useNitroHooks()` to guarantee having an instance. # Nightly Channel You can opt-in to the nightly release channel by updating your `package.json`: ```json { "devDependencies": { "nitro": "npm:nitro-nightly@latest" } } ``` Remove the lockfile (`package-lock.json`, `yarn.lock`, `pnpm-lock.yaml`, `bun.lock`, or `bun.lockb`) and reinstall the dependencies. ::important When using **Bun as package manager** in a mono-repo, you need to make sure nitro package is properly hoisted. :br ```toml [bunfig.toml] [install] publicHoistPattern = ["nitro*"] ``` :: ::important Avoid using ` install nitro-nightly`; it does not install correctly. If you encounter issues, delete your `node_modules` and lock files, then follow the steps above. :: # Deploy Nitro can generate different output formats suitable for different hosting providers from the same code base. Using built-in presets, you can easily configure Nitro to adjust its output format with almost no additional code or configuration! ## Default output The default production output preset is [Node.js server](https://nitro.build/deploy/runtimes/node). When running Nitro in development mode, Nitro will always use a special preset called `nitro-dev` using Node.js with ESM in an isolated Worker environment with behavior as close as possible to the production environment. ## Zero-Config Providers When deploying to production using CI/CD, Nitro tries to automatically detect the provider environment and set the right one without any additional configuration required. Currently, the providers below can be auto-detected with zero config. - [aws amplify](https://nitro.build/deploy/providers/aws-amplify) - [azure](https://nitro.build/deploy/providers/azure) - [cloudflare](https://nitro.build/deploy/providers/cloudflare) - [firebase app hosting](https://nitro.build/deploy/providers/firebase#firebase-app-hosting) - [netlify](https://nitro.build/deploy/providers/netlify) - [stormkit](https://nitro.build/deploy/providers/stormkit) - [vercel](https://nitro.build/deploy/providers/vercel) - [zeabur](https://nitro.build/deploy/providers/zeabur) ::warning For Turborepo users, zero config detection will be interferenced by its Strict Environment Mode. You may need to allowing the variables explictly or use its Loose Environment Mode (with `--env-mode=loose` flag). :: Other built-in providers are available with an explicit preset, including [zephyr](https://nitro.build/deploy/providers/zephyr). ## Changing the deployment preset If you need to build Nitro against a specific provider, you can target it by defining an environment variable named `NITRO_PRESET` or `SERVER_PRESET`, or by updating your Nitro [configuration](https://nitro.build/docs/configuration) or using `--preset` argument. Using the environment variable approach is recommended for deployments depending on CI/CD. **Example:** Defining a `NITRO_PRESET` environment variable ```bash nitro build --preset cloudflare_pages ``` **Example:** Updating the `nitro.config.ts` file ```ts import { defineNitroConfig } from "nitro/config"; export default defineNitroConfig({ preset: 'cloudflare_pages' }) ``` ## Compatibility date Deployment providers regularly update their runtime behavior. Nitro presets are updated to support these new features. To prevent breaking existing deployments, Nitro uses compatibility dates. These dates let you lock in behavior at the project creation time. You can also opt in to future updates when ready. When you create a new project, the `compatibilityDate` is set to the current date. This setting is saved in your project's configuration. You should update the compatibility date periodically. Always test your deployment thoroughly after updating. Below is a list of key dates and their effects. # Node.js **Preset:** `node_server` Node.js is the default nitro output preset for production builds and Nitro has native Node.js runtime support. Build project using nitro CLI: ```bash nitro build ``` When running `nitro build` with the Node server preset, the result will be an entry point that launches a ready-to-run Node server. To try output: ```bash $ node .output/server/index.mjs Listening on http://localhost:3000 ``` You can now deploy fully standalone `.output` directory to the hosting of your choice. ### Environment Variables You can customize server behavior using following environment variables: - `NITRO_PORT` or `PORT` (defaults to `3000`) - `NITRO_HOST` or `HOST` - `NITRO_UNIX_SOCKET` - if provided (a path to the desired socket file) the service will be served over the provided UNIX socket. - `NITRO_SSL_CERT` and `NITRO_SSL_KEY` - if both are present, this will launch the server in HTTPS mode. In the vast majority of cases, this should not be used other than for testing, and the Nitro server should be run behind a reverse proxy like nginx or Cloudflare which terminates SSL. - `NITRO_SHUTDOWN_DISABLED` - Disables the graceful shutdown feature when set to `'true'`. If it's set to `'true'`, the graceful shutdown is bypassed to speed up the development process. Defaults to `'false'`. - `NITRO_SHUTDOWN_SIGNALS` - Allows you to specify which signals should be handled. Each signal should be separated with a space. Defaults to `'SIGINT SIGTERM'`. - `NITRO_SHUTDOWN_TIMEOUT` - Sets the amount of time (in milliseconds) before a forced shutdown occurs. Defaults to `'30000'` milliseconds. - `NITRO_SHUTDOWN_FORCE` - When set to true, it triggers `process.exit()` at the end of the shutdown process. If it's set to `'false'`, the process will simply let the event loop clear. Defaults to `'true'`. ## Cluster mode **Preset:** `node_cluster` For more performance and leveraging multi-core handling, you can use cluster preset. ### Environment Variables In addition to environment variables from the `node_server` preset, you can customize behavior: - `NITRO_CLUSTER_WORKERS`: Number of cluster workers (default is Number of available cpu cores) ## Handler (advanced) **Preset:** `node_middleware` Nitro also has a more low-level preset that directly exports a middleware usable for custom servers. When running `nitro build` with the Node middleware preset, the result will be an entry point exporting a middleware handler. **Example:** ```js import { createServer } from 'node:http' import { listener } from './.output/server' const server = createServer(listener) server.listen(8080) ``` # Bun **Preset:** `bun` Nitro output is compatible with Bun runtime. While using default [Node.js](https://nitro.build/deploy/runtimes/node) you can also run the output in bun, using `bun` preset has advantage of better optimizations. After building with bun preset using `bun` as preset, you can run server in production using: ```bash bun run ./.output/server/index.mjs ``` :read-more{to="https://bun.sh"} # Deno **Preset:** `deno_server` You can build your Nitro server using Node.js to run within [Deno Runtime](https://deno.com/runtime){rel=""nofollow""} in a custom server. ```bash # Build with the deno NITRO preset NITRO_PRESET=deno_server npm run build # Start production server deno run --unstable --allow-net --allow-read --allow-env .output/server/index.ts ``` ## Deno Deploy :read-more{to="https://nitro.build/deploy/providers/deno-deploy"} # Alwaysdata **Preset:** `alwaysdata` :read-more{to="https://alwaysdata.com"} ## Set up application ### Pre-requisites ::steps{level="4"} #### [Register a new profile](https://www.alwaysdata.com/en/register/){rel=""nofollow""} on alwaysdata platform if you don't have one. #### Get a free 100Mb plan to host your app. :: ::note Keep in mind your *account name* will be used to provide you a default URL in the form of `account_name.alwaysdata.net`, so choose it wisely. You can also link your existing domains to your account later or register as many accounts under your profile as you need. :: ### Local deployment ::steps{level="4"} #### Build your project locally with `npm run build -- preset alwaysdata` #### [Upload your app](https://help.alwaysdata.com/en/remote-access/){rel=""nofollow""} to your account in its own directory (e.g. `$HOME/www/my-app`). You can use any protocol you prefer (SSH/FTP/WebDAV…) to do so. #### On your admin panel, [create a new site](https://admin.alwaysdata.com/site/add/){rel=""nofollow""} for your app with the following features:* *Addresses*: `[account_name].alwaysdata.net` * *Type*: Node.js * *Command*: `node .output/server/index.mjs` * *Working directory*: `www/my-app` (adapt it to your deployment path) * *Environment*: ```ini NITRO_PRESET=alwaysdata ``` * *Node.js version*: `Default version` is fine; pick no less than `20.0.0` (you can also [set your Node.js version globally](https://help.alwaysdata.com/en/languages/nodejs/configuration/#supported-versions){rel=""nofollow""}) * *Hot restart*: `SIGHUP`:read-more{title="Get more information about alwaysdata Node.js sites type" to="https://help.alwaysdata.com/en/languages/nodejs"} #### Your app is now live at `http(s)://[account_name].alwaysdata.net`. :: # AWS Lambda **Preset:** `aws_lambda` :read-more{title="AWS Lambda" to="https://aws.amazon.com/lambda/"} Nitro provides a built-in preset to generate output format compatible with [AWS Lambda](https://aws.amazon.com/lambda/){rel=""nofollow""}. The output entrypoint in `.output/server/index.mjs` is compatible with [AWS Lambda format](https://docs.aws.amazon.com/lex/latest/dg/lambda-input-response-format.html){rel=""nofollow""}. It can be used programmatically or as part of a deployment. ```ts import { handler } from './.output/server' // Use programmatically const { statusCode, headers, body } = handler({ rawPath: '/' }) ``` ## Inlining chunks Nitro output, by default uses dynamic chunks for lazy loading code only when needed. However this sometimes can not be ideal for performance. (See discussions in [nitrojs/nitro#650](https://github.com/nitrojs/nitro/pull/650){rel=""nofollow""}). You can enabling chunk inlining behavior using [`inlineDynamicImports`](https://nitro.build/config#inlinedynamicimports) config. ```ts [nitro.config.ts] import { defineNitroConfig } from "nitro/config"; export default defineNitroConfig({ inlineDynamicImports: true }); ``` ## Response streaming :read-more{title="Introducing AWS Lambda response streaming" to="https://aws.amazon.com/blogs/compute/introducing-aws-lambda-response-streaming/"} In order to enable response streaming, enable `awsLambda.streaming` flag: ```ts [nitro.config.ts] import { defineNitroConfig } from "nitro/config"; export default defineNitroConfig({ awsLambda: { streaming: true } }); ``` # AWS Amplify **Preset:** `aws_amplify` :read-more{title="AWS Amplify Hosting" to="https://aws.amazon.com/amplify"} ## Deploy to AWS Amplify Hosting ::tip Integration with this provider is possible with [zero configuration](https://nitro.build/deploy/#zero-config-providers). :: ::steps{level="4"} #### Login to the [AWS Amplify Hosting Console](https://console.aws.amazon.com/amplify/){rel=""nofollow""} #### Click on "Get Started" > Amplify Hosting (Host your web app) #### Select and authorize access to your Git repository provider and select the main branch #### Choose a name for your app, make sure build settings are auto-detected and optionally set requirement environment variables under the advanced section #### Optionally, select Enable SSR logging to enable server-side logging to your Amazon CloudWatch account #### Confirm configuration and click on "Save and Deploy" :: ## Advanced Configuration You can configure advanced options of this preset using `awsAmplify` option. ```ts [nitro.config.ts] import { defineNitroConfig } from "nitro/config"; export default defineNitroConfig({ awsAmplify: { // catchAllStaticFallback: true, // imageOptimization: { path: "/_image", cacheControl: "public, max-age=3600, immutable" }, // imageSettings: { ... }, // runtime: "nodejs18.x", // default: "nodejs18.x" | "nodejs16.x" | "nodejs20.x" } }) ``` ### `amplify.yml` You might need a custom `amplify.yml` file for advanced configuration. Here are two template examples: ::code-group ```yml [amplify.yml] version: 1 frontend: phases: preBuild: commands: - nvm use 18 && node --version - corepack enable && npx --yes nypm install build: commands: - pnpm build artifacts: baseDirectory: .amplify-hosting files: - "**/*" ``` ```yml [amplify.yml (monorepo)] version: 1 applications: - frontend: phases: preBuild: commands: - nvm use 18 && node --version - corepack enable && npx --yes nypm install build: commands: - pnpm --filter website1 build artifacts: baseDirectory: apps/website1/.amplify-hosting files: - '**/*' buildPath: / appRoot: apps/website1 ``` :: # Azure ## Azure static web apps **Preset:** `azure-swa` :read-more{title="Azure Static Web Apps" to="https://azure.microsoft.com/en-us/products/app-service/static"} ::note Integration with this provider is possible with [zero configuration](https://nitro.build/deploy/#zero-config-providers). :: [Azure Static Web Apps](https://azure.microsoft.com/en-us/products/app-service/static){rel=""nofollow""} are designed to be deployed continuously in a [GitHub Actions workflow](https://docs.microsoft.com/en-us/azure/static-web-apps/github-actions-workflow){rel=""nofollow""}. By default, Nitro will detect this deployment environment and enable the `azure` preset. ### Local preview Install [Azure Functions Core Tools](https://docs.microsoft.com/en-us/azure/azure-functions/functions-run-local){rel=""nofollow""} if you want to test locally. You can invoke a development environment to preview before deploying. ```bash NITRO_PRESET=azure npx nypm@latest build npx @azure/static-web-apps-cli start .output/public --api-location .output/server ``` ### Configuration Azure Static Web Apps are [configured](https://learn.microsoft.com/en-us/azure/static-web-apps/configuration){rel=""nofollow""} using the `staticwebapp.config.json` file. Nitro automatically generates this configuration file whenever the application is built with the `azure` preset. Nitro will automatically add the following properties based on the following criteria: | Property | Criteria | Default | | --------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- | | **[platform.apiRuntime](https://learn.microsoft.com/en-us/azure/static-web-apps/configuration#platform){rel=""nofollow""}** | Will automatically set to `node:16` or `node:14` depending on your package configuration. | `node:16` | | **[navigationFallback.rewrite](https://learn.microsoft.com/en-us/azure/static-web-apps/configuration#fallback-routes){rel=""nofollow""}** | Is always `/api/server` | `/api/server` | | **[routes](https://learn.microsoft.com/en-us/azure/static-web-apps/configuration#routes){rel=""nofollow""}** | All prerendered routes are added. Additionally, if you do not have an `index.html` file an empty one is created for you for compatibility purposes and also requests to `/index.html` are redirected to the root directory which is handled by `/api/server`. | `[]` | ### Custom configuration You can alter the Nitro generated configuration using `azure.config` option. Custom routes will be added and matched first. In the case of a conflict (determined if an object has the same route property), custom routes will override generated ones. ### Deploy from CI/CD via GitHub actions When you link your GitHub repository to Azure Static Web Apps, a workflow file is added to the repository. When you are asked to select your framework, select custom and provide the following information: | Input | Value | | -------------------- | ---------------- | | **app\_location** | '/' | | **api\_location** | '.output/server' | | **output\_location** | '.output/public' | If you miss this step, you can always find the build configuration section in your workflow and update the build configuration: ```yaml [.github/workflows/azure-static-web-apps-.yml] ###### Repository/Build Configurations ###### app_location: '/' api_location: '.output/server' output_location: '.output/public' ###### End of Repository/Build Configurations ###### ``` That's it! Now Azure Static Web Apps will automatically deploy your Nitro-powered application on push. If you are using runtimeConfig, you will likely want to configure the corresponding [environment variables on Azure](https://docs.microsoft.com/en-us/azure/static-web-apps/application-settings){rel=""nofollow""}. # Cleavr **Preset:** `cleavr` :read-more{title="cleavr.io" to="https://cleavr.io"} ::note Integration with this provider is possible with [zero configuration](https://nitro.build/deploy/#zero-config-providers). :: ## Set up your web app In your project, set Nitro preset to `cleavr`. ```js export default { nitro: { preset: 'cleavr' } } ``` Push changes to your code repository. **In your Cleavr panel:** ::steps{level="4"} #### Provision a new server #### Add a website, selecting **Nuxt 3** as the app type #### In web app > settings > Code Repo, point to your project's code repository :: You're now all set to deploy your project! # Cloudflare ## Cloudflare Workers **Preset:** `cloudflare_module` :read-more{title="Cloudflare Workers" to="https://developers.cloudflare.com/workers/"} ::note Integration with this provider is possible with [zero configuration](https://nitro.build/deploy#zero-config-providers) supporting [workers builds (beta)](https://developers.cloudflare.com/workers/ci-cd/builds/){rel=""nofollow""}. :: ::important To use Workers with Static Assets, you need a Nitro compatibility date set to `2024-09-19` or later. :: The following shows an example `nitro.config.ts` file for deploying a Nitro app to Cloudflare Workers. ```ts [nitro.config.ts] import { defineNitroConfig } from "nitro/config"; export default defineNitroConfig({ compatibilityDate: "2024-09-19", preset: "cloudflare_module", cloudflare: { deployConfig: true, nodeCompat: true } }) ``` By setting `deployConfig: true`, Nitro will automatically generate a `wrangler.json` for you with the correct configuration. If you need to add [Cloudflare Workers configuration](https://developers.cloudflare.com/workers/wrangler/configuration/){rel=""nofollow""}, such as [bindings](https://developers.cloudflare.com/workers/runtime-apis/bindings/){rel=""nofollow""}, you can either: - Set these in your Nitro config under the `cloudflare: { wrangler : {} }`. This has the same type as `wrangler.json`. - Provide your own `wrangler.json`. Nitro will merge your config with the appropriate settings, including pointing to the build output. ### Local Preview You can use [Wrangler](https://github.com/cloudflare/workers-sdk/tree/main/packages/wrangler){rel=""nofollow""} to preview your app locally: :pm-run{script="build"} :pm-x{command="wrangler dev"} ### Manual Deploy After having built your application you can manually deploy it with Wrangler. First make sure to be logged into your Cloudflare account: :pm-x{command="wrangler login"} Then you can deploy the application with: :pm-x{command="wrangler deploy"} ### Runtime Hooks You can use [runtime hooks](https://nitro.build/docs/plugins#nitro-runtime-hooks) below in order to extend [Worker handlers](https://developers.cloudflare.com/workers/runtime-apis/handlers/){rel=""nofollow""}. :read-more{to="https://nitro.build/docs/plugins#nitro-runtime-hooks"} - [`cloudflare:scheduled`](https://developers.cloudflare.com/workers/runtime-apis/handlers/scheduled/){rel=""nofollow""} - [`cloudflare:email`](https://developers.cloudflare.com/email-routing/email-workers/runtime-api/){rel=""nofollow""} - [`cloudflare:queue`](https://developers.cloudflare.com/queues/configuration/javascript-apis/#consumer){rel=""nofollow""} - [`cloudflare:tail`](https://developers.cloudflare.com/workers/runtime-apis/handlers/tail/){rel=""nofollow""} - `cloudflare:trace` ### Additional Exports You can add a `exports.cloudflare.ts` file to your project root to export additional handlers or properties to the Cloudflare Worker entrypoint. ```ts [exports.cloudflare.ts] export class MyWorkflow extends WorkflowEntrypoint { async run(event: WorkflowEvent, step: WorkflowStep) { // ... } } ``` Nitro will automatically detect this file and include its exports in the final build. ::warning The `exports.cloudflare.ts` file must not have a default export. :: You can also customize the entrypoint file location using the `cloudflare.exports` option in your `nitro.config.ts`: ```ts [nitro.config.ts] export default defineConfig({ cloudflare: { exports: "custom-exports-entry.ts" } }) ``` ### Scheduled Tasks (Cron Triggers) When using [Nitro tasks](https://nitro.build/docs/tasks) with `scheduledTasks`, Nitro automatically generates [Cron Triggers](https://developers.cloudflare.com/workers/configuration/cron-triggers/){rel=""nofollow""} in the wrangler config at build time. ```ts [nitro.config.ts] import { defineNitroConfig } from "nitro/config"; export default defineNitroConfig({ preset: "cloudflare_module", experimental: { tasks: true, }, scheduledTasks: { "* * * * *": ["cms:update"], "0 15 1 * *": ["db:cleanup"], }, cloudflare: { deployConfig: true, }, }) ``` No manual Wrangler configuration is needed - Nitro handles it for you. ## Cloudflare Pages **Preset:** `cloudflare_pages` :read-more{title="Cloudflare Pages" to="https://pages.cloudflare.com/"} ::note Integration with this provider is possible with [zero configuration](https://nitro.build/deploy#zero-config-providers). :: ::warning Cloudflare [Workers Module](https://nitro.build/#cloudflare-workers) is the new recommended preset for deployments. Please consider using the pages only if you need specific features. :: The following shows an example `nitro.config.ts` file for deploying a Nitro app to Cloudflare Pages. ```ts [nitro.config.ts] import { defineNitroConfig } from "nitro/config"; export default defineNitroConfig({ preset: "cloudflare_pages", cloudflare: { deployConfig: true, nodeCompat:true } }) ``` Nitro automatically generates a `_routes.json` file that controls which routes get served from files and which are served from the Worker script. The auto-generated routes file can be overridden with the config option `cloudflare.pages.routes` ([read more](https://developers.cloudflare.com/pages/platform/functions/routing/#functions-invocation-routes){rel=""nofollow""}). ### Local Preview You can use [Wrangler](https://github.com/cloudflare/workers-sdk/tree/main/packages/wrangler){rel=""nofollow""} to preview your app locally: :pm-run{script="build"} :pm-x{command="wrangler pages dev"} ### Manual Deploy After having built your application you can manually deploy it with Wrangler, in order to do so first make sure to be logged into your Cloudflare account: :pm-x{command="wrangler login"} Then you can deploy the application with: :pm-x{command="wrangler pages deploy"} ## Deploy within CI/CD using GitHub Actions Regardless on whether you're using Cloudflare Pages or Cloudflare Workers, you can use the [Wrangler GitHub actions](https://github.com/marketplace/actions/deploy-to-cloudflare-workers-with-wrangler){rel=""nofollow""} to deploy your application. ::note **Note:** Remember to [instruct Nitro to use the correct preset](https://nitro.build/deploy#changing-the-deployment-preset) (note that this is necessary for all presets including the `cloudflare_pages` one). :: ## Environment Variables Nitro allows you to universally access environment variables using `process.env` or `import.meta.env` or the runtime config. ::note Make sure to only access environment variables **within the event lifecycle** and not in global contexts since Cloudflare only makes them available during the request lifecycle and not before. :: **Example:** If you have set the `SECRET` and `NITRO_HELLO_THERE` environment variables set you can access them in the following way: ```ts import { defineHandler } from "nitro"; import { useRuntimeConfig } from "nitro/runtime-config"; console.log(process.env.SECRET) // note that this is in the global scope! so it doesn't actually work and the variable is undefined! export default defineHandler((event) => { // note that all the below are valid ways of accessing the above mentioned variables useRuntimeConfig().helloThere useRuntimeConfig().secret process.env.NITRO_HELLO_THERE import.meta.env.SECRET }); ``` ### Specify Variables in Development Mode For development, you can use a `.env` or `.env.local` file to specify environment variables: ```ini NITRO_HELLO_THERE="captain" SECRET="top-secret" ``` ::note **Note:** Make sure you add `.env` and `.env.local` to the `.gitignore` file so that you don't commit it as it can contain sensitive information. :: ### Specify Variables for local previews After build, when you try out your project locally with `wrangler dev` or `wrangler pages dev`, in order to have access to environment variables you will need to specify the in a `.dev.vars` file in the root of your project (as presented in the [Pages](https://developers.cloudflare.com/pages/functions/bindings/#interact-with-your-environment-variables-locally){rel=""nofollow""} and [Workers](https://developers.cloudflare.com/workers/configuration/environment-variables/#interact-with-environment-variables-locally){rel=""nofollow""} documentation). If you are using a `.env` or `.env.local` file while developing, your `.dev.vars` should be identical to it. ::note **Note:** Make sure you add `.dev.vars` to the `.gitignore` file so that you don't commit it as it can contain sensitive information. :: ### Specify Variables for Production For production, use the Cloudflare dashboard or the [`wrangler secret`](https://developers.cloudflare.com/workers/wrangler/commands/#secret){rel=""nofollow""} command to set environment variables and secrets. ### Specify Variables using `wrangler.toml`/`wrangler.json` You can specify a custom `wrangler.toml`/`wrangler.json` file and define vars inside. ::warning Note that this isn't recommend for sensitive data like secrets. :: **Example:** ::code-group ```ini [wrangler.toml] # Shared [vars] NITRO_HELLO_THERE="general" SECRET="secret" # Override values for `--env production` usage [env.production.vars] NITRO_HELLO_THERE="captain" SECRET="top-secret" ``` ```json [wrangler.json] { "vars": { "NITRO_HELLO_THERE": "general", "SECRET": "secret" }, "env": { "production": { "vars": { "NITRO_HELLO_THERE": "captain", "SECRET": "top-secret" } } } } ``` :: ## Direct access to Cloudflare bindings Bindings are what allows you to interact with resources from the Cloudflare platform, examples of such resources are key-value data storages ([KVs](https://developers.cloudflare.com/kv/){rel=""nofollow""}) and serverless SQL databases ([D1s](https://developers.cloudflare.com/d1/){rel=""nofollow""}). ::read-more For more details on Bindings and how to use them please refer to the Cloudflare [Pages](https://developers.cloudflare.com/pages/functions/bindings/){rel=""nofollow""} and [Workers](https://developers.cloudflare.com/workers/configuration/bindings/#bindings){rel=""nofollow""} documentation. :: ::tip Nitro provides high level API to interact with primitives such as [KV Storage](https://nitro.build/docs/storage) and [Database](https://nitro.build/docs/database) and you are highly recommended to prefer using them instead of directly depending on low-level APIs for usage stability. :: :read-more{title="Database Layer" to="https://nitro.build/docs/database"} :read-more{title="KV Storage" to="https://nitro.build/docs/storage"} In runtime, you can access bindings from the request event via `event.req.runtime.cloudflare.env`. This is for example how you can access a D1 binding: ```ts import { defineHandler } from "nitro"; defineHandler(async (event) => { const { env } = event.req.runtime.cloudflare const stmt = await env.MY_D1.prepare('SELECT id FROM table') const { results } = await stmt.all() }) ``` ### Access to the bindings in local dev To access bindings in dev mode, we first define them. You can do this in a `wrangler.jsonc`/`wrangler.json`/`wrangler.toml` file For example, to define a variable and a KV namespace in `wrangler.toml`: ::code-group ```ini [wrangler.toml] [vars] MY_VARIABLE="my-value" [[kv_namespaces]] binding = "MY_KV" id = "xxx" ``` ```json [wrangler.json] { "vars": { "MY_VARIABLE": "my-value", }, "kv_namespaces": [ { "binding": "MY_KV", "id": "xxx" } ] } ``` :: Next we install the required `wrangler` package (if not already installed): :pm-install{name="wrangler -D"} From this moment, when running :pm-run{script="dev"} you will be able to access the `MY_VARIABLE` and `MY_KV` from the request event just as illustrated above. #### Wrangler environments If you have multiple Wrangler environments, you can specify which Wrangler environment to use during Cloudflare dev emulation: ```ts [nitro.config.ts] import { defineNitroConfig } from "nitro/config"; export default defineNitroConfig({ preset: 'cloudflare_module', cloudflare: { dev: { environment: 'preview' } } }) ``` # Deno Deploy **Preset:** `deno_deploy` :read-more{title="Deno Deploy" to="https://deno.com/deploy"} ## Deploy with the CLI You can use [deployctl](https://deno.com/deploy/docs/deployctl){rel=""nofollow""} to deploy your app. Login to [Deno Deploy](https://dash.deno.com/account#access-tokens){rel=""nofollow""} to obtain a `DENO_DEPLOY_TOKEN` access token, and set it as an environment variable. ```bash # Build with the deno_deploy NITRO preset NITRO_PRESET=deno_deploy npm run build # Make sure to run the deployctl command from the output directory cd .output deployctl deploy --project=my-project server/index.ts ``` ## Deploy within CI/CD using GitHub actions You just need to include the deployctl GitHub Action as a step in your workflow. You do not need to set up any secrets for this to work. You do need to link your GitHub repository to your Deno Deploy project and choose the "GitHub Actions" deployment mode. You can do this in your project settings on [Deno Deploy](https://dash.deno.com){rel=""nofollow""}. Create the following workflow file in your `.github/workflows` directory: ```yaml [.github/workflows/deno_deploy.yml] name: deno-deploy on: push: branches: - main pull_request: branches: - main jobs: deploy: steps: - uses: actions/checkout@v5 - run: corepack enable - uses: actions/setup-node@v6 with: node-version: 18 cache: pnpm - run: pnpm install - run: pnpm build env: NITRO_PRESET: deno_deploy - name: Deploy to Deno Deploy uses: denoland/deployctl@v1 with: project: my-project entrypoint: server/index.ts root: .output ``` ## Deno runtime :read-more{to="https://nitro.build/deploy/runtimes/deno"} # DigitalOcean **Preset:** `digital_ocean` :read-more{title="Digital Ocean App Platform" to="https://docs.digitalocean.com/products/app-platform/"} ## Set up application ::steps{level="4"} #### Create a new Digital Ocean app following the [guide](https://docs.digitalocean.com/products/app-platform/how-to/create-apps/){rel=""nofollow""}. #### Next, you'll need to configure environment variables. In your app settings, ensure the following app-level environment variables are set:```bash NITRO_PRESET=digital_ocean ```:br[More information](https://docs.digitalocean.com/products/app-platform/how-to/use-environment-variables/){rel=""nofollow""}. #### You will need to ensure you set an `engines.node` field in your app's `package.json` to ensure Digital Ocean uses a supported version of Node.js:```json { "engines": { "node": "20.x" } } ```:br[See more information](https://docs.digitalocean.com/products/app-platform/languages-frameworks/nodejs/#node-version){rel=""nofollow""}. #### You'll also need to add a run command so Digital Ocean knows what command to run after a build. You can do so by adding a start script to your `package.json`:```json { "scripts": { "start": "node .output/server/index.mjs" } } ``` #### Finally, you'll need to add this start script to your Digital Ocean app's run command. Go to `Components > Settings > Commands`, click "Edit", then add `npm run start` :: Your app should be live at a Digital Ocean generated URL and you can now follow [the rest of the Digital Ocean deployment guide](https://docs.digitalocean.com/products/app-platform/how-to/manage-deployments/){rel=""nofollow""}. # Firebase ::note You will need to be on the [**Blaze plan**](https://firebase.google.com/pricing){rel=""nofollow""} (Pay as you go) to get started. :: ## Firebase app hosting Preset: `firebase_app_hosting` :read-more{title="Firebase App Hosting" to="https://firebase.google.com/docs/app-hosting"} ::tip You can integrate with this provider using [zero configuration](https://nitro.build/deploy/#zero-config-providers). :: ### Project setup ::steps{level="4"} #### Go to the Firebase [console](https://console.firebase.google.com/){rel=""nofollow""} and set up a new project. #### Select **Build > App Hosting** from the sidebar. * You may need to upgrade your billing plan at this step. #### Click **Get Started**. * Choose a region. * Import a GitHub repository (you’ll need to link your GitHub account). * Configure deployment settings (project root directory and branch), and enable automatic rollouts. * Choose a unique ID for your backend. #### Click Finish & Deploy to create your first rollout. :: When you deploy with Firebase App Hosting, the App Hosting preset will be run automatically at build time. # Flightcontrol **Preset:** `flightcontrol` :read-more{title="flightcontrol.dev" to="https://flightcontrol.dev?ref=nitro"} ## Set Up your flightcontrol account On a high level, the steps you will need to follow to deploy a project for the first time are: ::steps{level="4"} #### Create an account at [Flightcontrol](https://app.flightcontrol.dev/signup?ref=nitro){rel=""nofollow""} #### Create an account at [AWS](https://portal.aws.amazon.com/billing/signup){rel=""nofollow""} (if you don't already have one) #### Link your AWS account to the Flightcontrol #### Authorize the Flightcontrol GitHub App to access your chosen repositories, public or private. #### Create a Flightcontrol project with configuration via the Dashboard or with configuration via `flightcontrol.json`. :: ### Create a project with configuration via the dashboard ::steps{level="4"} #### Create a Flightcontrol project from the Dashboard. Select a repository for the source. #### Select the `GUI` config type. #### Select the Nuxt preset. This preset will also work for any Nitro-based applications. #### Select your preferred AWS server size. #### Submit the new project form. :: ### Create a project with configuration via `flightcontrol.json` ::steps{level="4"} #### Create a Flightcontrol project from your dashboard. Select a repository for the source. #### Select the `flightcontrol.json` config type. #### Add a new file at the root of your repository called `flightcontrol.json`. Here is an example configuration that creates an AWS fargate service for your app: :: ```json [flightcontrol.json] { "$schema": "https://app.flightcontrol.dev/schema.json", "environments": [ { "id": "production", "name": "Production", "region": "us-west-2", "source": { "branch": "main" }, "services": [ { "id": "nitro", "buildType": "nixpacks", "name": "My Nitro site", "type": "fargate", "domain": "www.yourdomain.com", "outputDirectory": ".output", "startCommand": "node .output/server/index.mjs", "cpu": 0.25, "memory": 0.5 } ] } ] } ``` 4. Submit the new project form. ::read-more{to="https://www.flightcontrol.dev/docs?ref=nitro"} Learn more about Flightcontrol's [configuration](https://www.flightcontrol.dev/docs?ref=nitro){rel=""nofollow""}. :: # Genezio **Preset:** `genezio` :read-more{title="Genezio" to="https://genezio.com"} ::important 🚧 This preset is currently experimental. :: ## 1. Project Setup Create `genezio.yaml` file: ```yaml # The name of the project. name: nitro-app # The version of the Genezio YAML configuration to parse. yamlVersion: 2 backend: # The root directory of the backend. path: .output/ # Information about the backend's programming language. language: # The name of the programming language. name: js # The package manager used by the backend. packageManager: npm # Information about the backend's functions. functions: # The name (label) of the function. - name: nitroServer # The path to the function's code. path: server/ # The name of the function handler handler: handler # The entry point for the function. entry: index.mjs ``` ::read-more --- to: https://genezio.com/docs/project-structure/genezio-configuration-file/ --- To further customize the file to your needs, you can consult the [official documentation](https://genezio.com/docs/project-structure/genezio-configuration-file/){rel=""nofollow""}. :: ## 2. Deploy your project Build with the genezio nitro preset: ```bash NITRO_PRESET=genezio npm run build ``` Deploy with [`genezio`](https://npmjs.com/package/genezio){rel=""nofollow""} cli: :pm-x{command="genezio deploy"} ::read-more --- title: Backend Environment Variables to: https://genezio.com/docs/project-structure/backend-environment-variables --- To set environment viarables, please check out [Genezio - Environment Variables](https://genezio.com/docs/project-structure/backend-environment-variables){rel=""nofollow""}. :: ## 3. Monitor your project You can monitor and manage your application through the [Genezio App Dashboard](https://app.genez.io/dashboard){rel=""nofollow""}. The dashboard URL, also provided after deployment, allows you to access comprehensive views of your project's status and logs. # GitHub Pages **Preset:** `github_pages` :read-more{title="GitHub Pages" to="https://pages.github.com/"} ## Setup Follow the steps to [create a GitHub Pages site](https://docs.github.com/en/pages/getting-started-with-github-pages/creating-a-github-pages-site){rel=""nofollow""}. ## Deployment Here is an example GitHub Actions workflow to deploy your site to GitHub Pages using the `github_pages` preset: ```yaml [.github/workflows/deploy.yml] # https://github.com/actions/deploy-pages#usage name: Deploy to GitHub Pages on: workflow_dispatch: push: branches: - main jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - run: corepack enable - uses: actions/setup-node@v6 with: node-version: "18" - run: npx nypm install - run: npm run build env: NITRO_PRESET: github_pages - name: Upload artifact uses: actions/upload-pages-artifact@v1 with: path: ./.output/public # Deployment job deploy: # Add a dependency to the build job needs: build # Grant GITHUB_TOKEN the permissions required to make a Pages deployment permissions: pages: write # to deploy to Pages id-token: write # to verify the deployment originates from an appropriate source # Deploy to the github_pages environment environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} # Specify runner + deployment step runs-on: ubuntu-latest steps: - name: Deploy to GitHub Pages id: deployment uses: actions/deploy-pages@v1 ``` # GitLab Pages **Preset:** `gitlab_pages` :read-more{title="GitLab Pages" to="https://pages.github.com/"} ## Setup Follow the steps to [create a GitLab Pages site](https://docs.gitlab.com/ee/user/project/pages/#getting-started){rel=""nofollow""}. ## Deployment 1. Here is an example GitLab Pages workflow to deploy your site to GitLab Pages: ```yaml [.gitlab-ci.yml] image: node:lts before_script: - npx nypm install pages: cache: paths: - node_modules/ variables: NITRO_PRESET: gitlab_pages script: - npm run build artifacts: paths: - .output/public publish: .output/public rules: # This ensures that only pushes to the default branch # will trigger a pages deploy - if: $CI_COMMIT_REF_NAME == $CI_DEFAULT_BRANCH ``` # Heroku **Preset:** `heroku` :read-more{title="heroku.com" to="https://heroku.com/"} ## Using the heroku CLI ::steps{level="4"} #### Create a new Heroku app.```bash heroku create myapp ``` #### Configure Heroku to use the nodejs buildpack.```bash heroku buildpacks:set heroku/nodejs ``` #### Configure your app.```bash heroku config:set NITRO_PRESET=heroku ``` #### Ensure you have `start` and `build` commands in your `package.json` file.```json5 "scripts": { "build": "nitro build", // or `nuxt build` if using nuxt "start": "node .output/server/index.mjs" } ``` :: ## With nginx ::steps{level="4"} #### Add the heroku Nginx buildpack [here](https://github.com/heroku/heroku-buildpack-nginx.git){rel=""nofollow""} #### Change to the 'node' preset in your `nitro.config````json5 "nitro": { "preset":"node", } ``` #### From the **Existing app** section of buildpack doc, 2 key steps are required to get things running :br Step 1: Listen on a socket at 'tmp/nginx.socket' Step 2: Create a file '/tmp/app-initialized' when your app is ready to accept connections #### Create custom app runner, eg: apprunner.mjs at the root of the project (or any other preferred location), in this file, create a server, using the listener generated by the node preset, then listen on the socket as detailed in the buildpack doc```ts import { createServer } from 'node:http' import { listener } from './.output/server/index.mjs' const server = createServer(listener) server.listen('/tmp/nginx.socket') //following the buildpack doc ``` #### To create the 'tmp/app-initialized' file, use a nitro plugin, create file 'initServer.ts' at the root of the project (or any other preferred location)```ts import fs from "fs" export default definePlugin((nitroApp) => { if((process.env.NODE_ENV || 'development') != 'development') { fs.openSync('/tmp/app-initialized', 'w') } }) ``` #### Finally, create file 'Procfile' at the root of the project, with the Procfile, we tell heroku to start nginx and use the custom apprunner.mjs to start the server :br web: bin/start-nginx node apprunner.mjs #### Bonus: create file 'config/nginx.conf.erb' to customize your nginx config. With the node preset, by default, static files handlers will not be generated, you can use nginx to server static files, just add the right location rule to the server block(s), or, force the node preset to generate handlers for the static files by setting serveStatic to true. :: # IIS ## Using [IISnode](https://github.com/Azure/iisnode){rel=""nofollow""} **Preset:** `iis_node` ::steps{level="4"} #### Install the latest LTS version of [Node.js](https://nodejs.org/en/){rel=""nofollow""} on your Windows Server. #### Install [IISnode](https://github.com/azure/iisnode/releases){rel=""nofollow""} #### Install [IIS `URLRewrite` Module](https://www.iis.net/downloads/microsoft/url-rewrite){rel=""nofollow""}. #### In IIS, add `.mjs` as a new mime type and set its content type to `application/javascript`. #### Deploy the contents of your `.output` folder to your website in IIS. :: ## Using IIS handler **Preset:** `iis_handler` You can use IIS http handler directly. ::steps{level="4"} #### Install the latest LTS version of [Node.js](https://nodejs.org/en/){rel=""nofollow""} on your Windows Server. #### Install [IIS `HttpPlatformHandler` Module](https://www.iis.net/downloads/microsoft/httpplatformhandler){rel=""nofollow""} #### Copy your `.output` directory into the Windows Server, and create a website on IIS pointing to that exact directory. :: ## IIS config options ```ts [nitro.config.ts] import { defineNitroConfig } from "nitro/config"; export default defineNitroConfig({ // IIS options default iis: { // merges in a pre-existing web.config file to the nitro default file mergeConfig: true, // overrides the default nitro web.config file all together overrideConfig: false, }, }); ``` # Koyeb **Preset:** `koyeb` :read-more{to="https://www.koyeb.com"} ## Using the control panel ::steps{level="4"} #### In the [Koyeb control panel](https://app.koyeb.com/){rel=""nofollow""}, click **Create App**. #### Choose **GitHub** as your deployment method. #### Choose the GitHub **repository** and **branch** containing your application code. #### Name your Service. #### If you did not add a `start` command to your `package.json` file, under the **Build and deployment settings**, toggle the override switch associated with the run command field. In the **Run command** field, enter:```bash node .output/server/index.mjs` ``` #### In the **Advanced** section, click **Add Variable** and add a `NITRO_PRESET` variable set to `koyeb`. #### Name the App. #### Click the **Deploy** button. :: ## Using the Koyeb CLI ::steps{level="4"} #### Follow the instructions targeting your operating system to [install the Koyeb CLI client](https://www.koyeb.com/docs/cli/installation){rel=""nofollow""} with an installer. Alternatively, visit the [releases page on GitHub](https://github.com/koyeb/koyeb-cli/releases){rel=""nofollow""} to directly download required files. #### Create a Koyeb API access token by visiting the [API settings for your organization](https://app.koyeb.com/settings/api){rel=""nofollow""} in the Koyeb control panel. #### Log into your account with the Koyeb CLI by typing:```bash koyeb login ```:br Paste your API credentials when prompted. #### Deploy your Nitro application from a GitHub repository with the following command. Be sure to substitute your own values for ``, ``, and ``:```bash koyeb app init \ --git github.com// \ --git-branch main \ --git-run-command "node .output/server/index.mjs" \ --ports 3000:http \ --routes /:3000 \ --env PORT=3000 \ --env NITRO_PRESET=koyeb ``` :: ## Using a docker container ::steps{level="4"} #### Create a `.dockerignore` file in the root of your project and add the following lines:```text Dockerfile .dockerignore node_modules npm-debug.log .nitro .output .git dist README.md ``` #### Add a `Dockerfile` to the root of your project:```text FROM node:18-alpine AS base FROM base AS deps RUN apk add --no-cache libc6-compat WORKDIR /app COPY package.json package-lock.json ./ RUN npm ci FROM base AS builder WORKDIR /app COPY --from=deps /app/node_modules ./node_modules COPY . . RUN npm run build && npm cache clean --force FROM base AS runner WORKDIR /app RUN addgroup --system --gid 1001 nodejs RUN adduser --system --uid 1001 nitro COPY --from=builder /app . USER nitro EXPOSE 3000 ENV PORT 3000 CMD ["npm", "run", "start"] ``` :: The Dockerfile above provides the minimum requirements to run the Nitro application. You can easily extend it depending on your needs. You will then need to push your Docker image to a registry. You can use [Docker Hub](https://hub.docker.com/){rel=""nofollow""} or [GitHub Container Registry](https://docs.github.com/en/packages/guides/about-github-container-registry){rel=""nofollow""} for example. In the Koyeb control panel, use the image and the tag field to specify the image you want to deploy. You can also use the [Koyeb CLI](https://www.koyeb.com/docs/build-and-deploy/cli/installation){rel=""nofollow""} Refer to the Koyeb [Docker documentation](https://www.koyeb.com/docs/build-and-deploy/prebuilt-docker-images){rel=""nofollow""} for more information. # Netlify **Preset:** `netlify` :read-more{title="Netlify Functions" to="https://www.netlify.com/platform/core/functions/"} ::note Integration with this provider is possible with [zero configuration](https://nitro.build/deploy/#zero-config-providers). :: Normally, the deployment to Netlify does not require any configuration. Nitro will auto-detect that you are in a [Netlify](https://www.netlify.com){rel=""nofollow""} build environment and build the correct version of your server. For new sites, Netlify will detect that you are using Nitro and set the publish directory to `dist` and build command to `npm run build`. If you are upgrading an existing site you should check these and update them if needed. If you want to add custom redirects, you can do so with [`routeRules`](https://nitro.build/config#routerules) or by adding a [`_redirects`](https://docs.netlify.com/routing/redirects/#syntax-for-the-redirects-file){rel=""nofollow""} file to your `public` directory. For deployment, just push to your git repository [as you would normally do for Netlify](https://docs.netlify.com/configure-builds/get-started/){rel=""nofollow""}. ::note{type="note"} Make sure the publish directory is set to `dist` when creating a new project. :: ## Netlify edge functions **Preset:** `netlify_edge` Netlify Edge Functions use Deno and the powerful V8 JavaScript runtime to let you run globally distributed functions for the fastest possible response times. :read-more{title="Netlify Edge functions" to="https://docs.netlify.com/edge-functions/overview/"} Nitro output can directly run the server at the edge. Closer to your users. ::note{type="note"} Make sure the publish directory is set to `dist` when creating a new project. :: ## Custom deploy configuration You can provide additional deploy configuration using the `netlify` key inside `nitro.config`. It will be merged with built-in auto-generated config. Currently the only supported value is `images.remote_images`, for [configuring Netlify Image CDN](https://docs.netlify.com/image-cdn/create-integration/){rel=""nofollow""}. # Platform.sh **Preset:** `platform_sh` :read-more{to="https://platform.sh"} ## Setup First, create a new project on platform.sh and link it to the repository you want to auto-deploy with. Then in repository create `.platform.app.yaml` file: ```yaml [.platform.app.yaml] name: nitro-app type: 'nodejs:20' disk: 128 web: commands: start: "node .output/server/index.mjs" build: flavor: none hooks: build: | corepack enable npx nypm install NITRO_PRESET=platform_sh npm run build mounts: '.data': source: local source_path: .data ``` :read-more{title="Complete list of all available properties" to="https://docs.platform.sh/create-apps/app-reference.html"} :read-more{title="Complete list of all available properties" to="https://unjs.io/blog/2023-08-25-nitro-2.6#default-persistent-data-storage"} # Render.com **Preset:** `render_com` :read-more{title="render.com" to="https://render.com"} ## Set up application ::steps{level="4"} #### [Create a new Web Service](https://dashboard.render.com/select-repo?type=web){rel=""nofollow""} and select the repository that contains your code. #### Ensure the 'Node' environment is selected. #### Update the start command to `node .output/server/index.mjs` #### Click 'Advanced' and add an environment variable with `NITRO_PRESET` set to `render_com`. You may also need to add a `NODE_VERSION` environment variable set to `20` for the build to succeed ([docs](https://render.com/docs/node-version){rel=""nofollow""}). #### Click 'Create Web Service'. :: ## Infrastructure as Code (IaC) 1. Create a file called `render.yaml` with following content at the root of your repository. > This file followed by [Infrastructure as Code](https://render.com/docs/infrastructure-as-code){rel=""nofollow""} on Render ```yaml services: - type: web name: env: node branch: main startCommand: node .output/server/index.mjs buildCommand: npx nypm install && npm run build envVars: - key: NITRO_PRESET value: render_com ``` 1. [Create a new Blueprint Instance](https://dashboard.render.com/select-repo?type=blueprint){rel=""nofollow""} and select the repository containing your `render.yaml` file. You should be good to go! # StormKit **Preset:** `stormkit` :read-more{title="Stormkit" to="https://www.stormkit.io"} ::note Integration with [Stormkit](https://www.stormkit.io/){rel=""nofollow""} is possible with [zero configuration](https://nitro.build/deploy#zero-config-providers). :: ## Setup Follow the steps to [create a new app](https://app.stormkit.io/apps/new){rel=""nofollow""} on Stormkit. ![Create a new app on Stormkit](https://nitro.build/images/stormkit-new-app.png) ## Deployment By default, Stormkit will deploy your apps automatically when you push changes to your main branch. But to trigger a manual deploy (for example, you might do this for the very first deployment), you may click `Deploy now`. ![Trigger a manual deploy with Deploy Now](https://nitro.build/images/stormkit-deploy.png) # Vercel **Preset:** `vercel` :read-more{title="Vercel Framework Support" to="https://vercel.com/docs/frameworks"} ::note Integration with this provider is possible with [zero configuration](https://nitro.build/deploy/#zero-config-providers). :: ## Getting started Deploying to Vercel comes with the following features: - [Preview deployments](https://vercel.com/docs/deployments/environments){rel=""nofollow""} - [Fluid compute](https://vercel.com/docs/fluid-compute){rel=""nofollow""} - [Observability](https://vercel.com/docs/observability){rel=""nofollow""} - [Vercel Firewall](https://vercel.com/docs/vercel-firewall){rel=""nofollow""} And much more. Learn more in [the Vercel documentation](https://vercel.com/docs){rel=""nofollow""}. ### Deploy with Git Vercel supports Nitro with zero-configuration. [Deploy Nitro to Vercel now](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fvercel%2Fvercel%2Ftree%2Fmain%2Fexamples%2Fnitro){rel=""nofollow""}. ## API routes Nitro `/api` directory isn't compatible with Vercel. Instead, you should use: - `routes/api/` for standalone usage ## Bun runtime :read-more{title="Vercel" to="https://vercel.com/docs/functions/runtimes/bun"} You can use [Bun](https://bun.com){rel=""nofollow""} instead of Node.js by specifying the runtime using the `vercel.functions` key inside `nitro.config`: ```ts [nitro.config.ts] export default defineNitroConfig({ vercel: { functions: { runtime: "bun1.x" } } }) ``` Alternatively, Nitro also detects Bun automatically if you specify a `bunVersion` property in your `vercel.json`: ```json [vercel.json] { "$schema": "https://openapi.vercel.sh/vercel.json", "bunVersion": "1.x" } ``` ## Proxy route rules Nitro automatically optimizes `proxy` route rules on Vercel by generating [CDN-level rewrites](https://vercel.com/docs/rewrites){rel=""nofollow""} at build time. This means matching requests are proxied at the edge without invoking a serverless function, reducing latency and cost. ```ts [nitro.config.ts] export default defineNitroConfig({ routeRules: { // Proxied at CDN level — no function invocation "/api/**": { proxy: "https://api.example.com/**", }, }, }); ``` ### When CDN rewrites apply A proxy rule is offloaded to a Vercel CDN rewrite when **all** of the following are true: - The target is an **external URL** (starts with `http://` or `https://`). - No advanced `ProxyOptions` are set on the rule. ### Fallback to runtime proxy When the proxy rule uses any of the following `ProxyOptions`, Nitro keeps it as a runtime proxy handled by the serverless function: - `headers` — custom headers on the outgoing request to the upstream - `forwardHeaders` / `filterHeaders` — header filtering - `fetchOptions` — custom fetch options - `cookieDomainRewrite` / `cookiePathRewrite` — cookie manipulation - `onResponse` — response callback ::note Response headers defined on the route rule via the `headers` option are still applied to CDN-level rewrites. Only request-level `ProxyOptions.headers` (sent to the upstream) require a runtime proxy. :: ## Scheduled tasks (Cron Jobs) :read-more{title="Vercel Cron Jobs" to="https://vercel.com/docs/cron-jobs"} Nitro automatically converts your [`scheduledTasks`](https://nitro.build/docs/tasks#scheduled-tasks) configuration into [Vercel Cron Jobs](https://vercel.com/docs/cron-jobs){rel=""nofollow""} at build time. Define your schedules in your Nitro config and deploy - no manual `vercel.json` cron configuration required. ```ts [nitro.config.ts] import { defineNitroConfig } from "nitro/config"; export default defineNitroConfig({ experimental: { tasks: true }, scheduledTasks: { // Run `cms:update` every hour '0 * * * *': ['cms:update'], // Run `db:cleanup` every day at midnight '0 0 * * *': ['db:cleanup'] } }) ``` ### Secure cron job endpoints :read-more{title="Securing cron jobs" to="https://vercel.com/docs/cron-jobs/manage-cron-jobs#securing-cron-jobs"} To prevent unauthorized access to the cron handler, set a `CRON_SECRET` environment variable in your Vercel project settings. When `CRON_SECRET` is set, Nitro validates the `Authorization` header on every cron invocation. ## Custom build output configuration You can provide additional [build output configuration](https://vercel.com/docs/build-output-api/v3){rel=""nofollow""} using `vercel.config` key inside `nitro.config`. It will be merged with built-in auto-generated config. ## On-Demand incremental static regeneration (ISR) On-demand revalidation allows you to purge the cache for an ISR route whenever you want, foregoing the time interval required with background revalidation. To revalidate a page on demand: ::steps{level="4"} #### Create an Environment Variable which will store a revalidation secret* You can use the command `openssl rand -base64 32` or [Generate a Secret](https://generate-secret.vercel.app/32){rel=""nofollow""} to generate a random value. #### Update your configuration:```ts \[nitro.config.ts] import { defineNitroConfig } from "nitro/config"; export default defineNitroConfig({ vercel: { config: { bypassToken: process.env.VERCEL_BYPASS_TOKEN } } }) ``` #### To trigger "On-Demand Incremental Static Regeneration (ISR)" and revalidate a path to a Prerender Function, make a GET or HEAD request to that path with a header of x-prerender-revalidate: `bypassToken`. When that Prerender Function endpoint is accessed with this header set, the cache will be revalidated. The next request to that function should return a fresh response. :: ### Fine-grained ISR config via route rules By default, query params affect cache keys but are not passed to the route handler unless specified. You can pass an options object to `isr` route rule to configure caching behavior. - `expiration`: Expiration time (in seconds) before the cached asset will be re-generated by invoking the Serverless Function. Setting the value to `false` (or `isr: true` route rule) means it will never expire. - `group`: Group number of the asset. Prerender assets with the same group number will all be re-validated at the same time. - `allowQuery`: List of query string parameter names that will be cached independently. - If an empty array, query values are not considered for caching. - If `undefined` each unique query value is cached independently. - For wildcard `/**` route rules, `url` is always added - `passQuery`: When `true`, the query string will be present on the `request` argument passed to the invoked function. The `allowQuery` filter still applies. - `exposeErrBody`: When `true`, expose the response body regardless of status code including error status codes. (default `false` ```ts export default defineNitroConfig({ routeRules: { "/products/**": { isr: { allowQuery: ["q"], passQuery: true, exposeErrBody: true }, }, }, }); ``` # Zeabur **Preset:** `zeabur` :read-more{title="Zeabur" to="https://zeabur.com"} ::note Integration with this provider is possible with [zero configuration](https://nitro.build/deploy/#zero-config-providers). :: ## Deploy using git ::steps{level="4"} #### Push your code to your git repository (Currently only GitHub supported). #### [Import your project](https://zeabur.com/docs/get-started){rel=""nofollow""} into Zeabur. #### Zeabur will detect that you are using Nitro and will enable the correct settings for your deployment. #### Your application is deployed! :: # Zephyr Cloud **Preset:** `zephyr` :read-more{title="Zephyr Cloud Docs" to="https://docs.zephyr-cloud.io"} Zephyr support is built into Nitro through the `zephyr` preset. For most Zephyr-specific topics such as BYOC, cloud integrations, environments, and CI/CD authentication, refer to the [Zephyr Cloud docs](https://docs.zephyr-cloud.io){rel=""nofollow""}. ::note Zephyr is a little different from most Nitro deployment providers. Instead of targeting a single hosting vendor directly, Zephyr acts as a deployment control plane on top of either Zephyr-managed infrastructure or your own cloud integrations. :: ## BYOC model Zephyr supports a BYOC (Bring Your Own Cloud) model. In Zephyr's architecture, the control plane stays managed by Zephyr, while the data plane (workers and storage) runs in your cloud accounts. This lets you keep Zephyr's deployment workflow while using any supported Zephyr cloud integration. See the [Zephyr BYOC docs](https://docs.zephyr-cloud.io/features/byoc){rel=""nofollow""} for the current list of supported providers. ## Deploy with Nitro CLI Use Nitro's deploy command to build and upload your app to Zephyr in one step: ```bash npx nitro deploy --preset zephyr ``` Nitro will upload the generated output using `zephyr-agent`. If `zephyr-agent` is missing, Nitro will prompt to install it locally and will install it automatically in CI. ## Deploy during build Zephyr is a little different here from most Nitro providers: we recommend enabling deployment during `nitro build` and treating build as the primary deployment step. If your CI pipeline already runs `nitro build`, enable deployment during the build step: ```ts [nitro.config.ts] import { defineNitroConfig } from "nitro/config"; export default defineNitroConfig({ preset: "zephyr", zephyr: { deployOnBuild: true, }, }); ``` Then your normal build command is enough: :pm-run{script="build"} After the build finishes, Nitro uploads the generated output to Zephyr, deploys it to the edge, and prints the deployment URL: ```txt ◐ Building [Nitro] (preset: zephyr, compatibility: YYYY-MM-DD) ... ZEPHYR Uploaded local snapshot in 110ms ZEPHYR Deployed to Zephyr's edge in 700ms. ZEPHYR ZEPHYR https://my-app.zephyrcloud.app ``` ## CI authentication Zephyr requires an API token for non-interactive deployments. The example below uses the simpler personal-token style setup with `ZE_SECRET_TOKEN` together with `zephyr.deployOnBuild`. ```yaml [.github/workflows/deploy.yml] name: Deploy with Zephyr on: push: branches: [main] jobs: deploy: runs-on: ubuntu-latest env: ZE_SECRET_TOKEN: ${{ secrets.ZEPHYR_AUTH_TOKEN }} steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 20 - run: npm ci - run: npm run build ``` For more advanced CI/CD setups, Zephyr also documents organization-level server-token authentication using `ZE_SERVER_TOKEN`. See the [Zephyr CI/CD server token docs](https://docs.zephyr-cloud.io/features/ci-cd-server-token){rel=""nofollow""}. ## Options ### `zephyr.deployOnBuild` Deploy to Zephyr during `nitro build` when using the `zephyr` preset. - Default: `false` # Zerops **Preset:** `zerops` :read-more{title="zerops.io" to="https://zerops.io"} ::important 🚧 This preset is currently experimental. :: Zerops supports deploying both static and server-side rendered apps with a simple configuration file in your project root. ## Starter templates If you want to quckly get started with zerops and nitro you can use repositories [`zeropsio/recipe-nitro-nodejs`](https://github.com/zeropsio/recipe-nitro-nodejs){rel=""nofollow""} and [`zeropsio/recipe-nitro-static`](https://github.com/zeropsio/recipe-nitro-static){rel=""nofollow""} starter templates. ## Project setup Projects and services can be added either through [project add wizard](https://app.zerops.io/dashboard/project-add){rel=""nofollow""} or imported using `zerops-project-import.yml`. ::code-group ```yml [zerops-project-import.yml (node.js)] project: name: nitro-app services: - hostname: app type: nodejs@20 ``` ```yml [zerops-project-import.yml (static)] project: name: nitro-app services: - hostname: app type: static ``` :: Then create a `zerops.yml` config in your project root: ::code-group ```yml [zerops.yml (node.js)] zerops: - setup: app build: base: nodejs@20 envVariables: NITRO_PRESET: zerops buildCommands: - pnpm i - pnpm run build deployFiles: - .output - package.json - node_modules run: base: nodejs@20 ports: - port: 3000 httpSupport: true start: node .output/server/index.mjs ``` ```yml [zerops.yml (static)] zerops: - setup: app build: base: nodejs@20 envVariables: NITRO_PRESET: zerops-static buildCommands: - pnpm i - pnpm build deployFiles: - .zerops/output/static/~ run: base: static ``` :: Now you can trigger the [build & deploy pipeline using the Zerops CLI](https://nitro.build/#building-deploying-your-app) or by connecting the app service with your [GitHub](https://docs.zerops.io/references/github-integration/){rel=""nofollow""} / [GitLab](https://docs.zerops.io/references/gitlab-integration){rel=""nofollow""} repository from inside the service detail. ## Build and deploy Open [Settings > Access Token Management](https://app.zerops.io/settings/token-management){rel=""nofollow""} in the Zerops app and generate a new access token. Log in using your access token with the following command: :pm-x{command="@zerops/zcli login "} Navigate to the root of your app (where `zerops.yml` is located) and run the following command to trigger the deploy: :pm-x{command="@zerops/zcli push"} Your code can be deployed automatically on each commit or a new tag by connecting the service with your [GitHub](https://docs.zerops.io/references/gitlab-integration){rel=""nofollow""} / [GitLab](https://docs.zerops.io/references/gitlab-integration){rel=""nofollow""} repository. This connection can be set up in the service detail. :read-more{title="Zerops Documentation" to="https://docs.zerops.io/"} # Config :read-more{to="https://nitro.build/guide/configuration"} ## General ### `preset` Use `preset` option or `NITRO_PRESET` environment variable for custom **production** preset. Preset for development mode is always `nitro_dev` and default `node_server` for production building a standalone Node.js server. The preset will automatically be detected when the `preset` option is not set and running in known environments. ```ts export default defineNitroConfig({ preset: "cloudflare_pages", // deploy to Cloudflare Pages }); ``` ### `debug` - Default: `false`{.shiki,shiki-themes,github-light,github-dark,github-dark lang="ts"} (`true`{.shiki,shiki-themes,github-light,github-dark,github-dark lang="ts"} when `DEBUG` environment variable is set) Enable debug mode for verbose logging and additional development information. ```ts export default defineNitroConfig({ debug: true, }); ``` ### `logLevel` - Default: `3`{.shiki,shiki-themes,github-light,github-dark,github-dark lang="ts"} (`1`{.shiki,shiki-themes,github-light,github-dark,github-dark lang="ts"} when the testing environment is detected) Log verbosity level. See [consola](https://github.com/unjs/consola?tab=readme-ov-file#log-level){rel=""nofollow""} for more information. ```ts export default defineNitroConfig({ logLevel: 4, // verbose logging }); ``` ### `runtimeConfig` - Default: `{ nitro: { ... }, ...yourOptions }`{.shiki,shiki-themes,github-light,github-dark,github-dark lang="ts"} Server runtime configuration. **Note:** `nitro` namespace is reserved. ```ts export default defineNitroConfig({ runtimeConfig: { apiSecret: "default-secret", // override with NITRO_API_SECRET }, }); ``` ### `compatibilityDate` Deployment providers introduce new features that Nitro presets can leverage, but some of them need to be explicitly opted into. Set it to latest tested date in `YYYY-MM-DD` format to leverage latest preset features. If this configuration is not provided, Nitro will use `"latest"` behavior by default. ```ts export default defineNitroConfig({ compatibilityDate: "2025-01-01", }); ``` ### `static` - Default: `false`{.shiki,shiki-themes,github-light,github-dark,github-dark lang="ts"} Enable static site generation mode. ```ts export default defineNitroConfig({ static: true, // prerender all routes }); ``` ## Features ### `features` - Default: `{}`{.shiki,shiki-themes,github-light,github-dark,github-dark lang="ts"} Enable built-in features. #### `runtimeHooks` - Default: auto-detected (enabled if there is at least one nitro plugin) Enable runtime hooks for request and response. #### `websocket` - Default: `false`{.shiki,shiki-themes,github-light,github-dark,github-dark lang="ts"} Enable WebSocket support. ```ts export default defineNitroConfig({ features: { runtimeHooks: true, websocket: true, // enable WebSocket support }, }); ``` ### `experimental` - Default: `{}` Enable experimental features. #### `openAPI` - Default: `false`{.shiki,shiki-themes,github-light,github-dark,github-dark lang="ts"} Enable `/_scalar`, `/_swagger` and `/_openapi.json` endpoints. ::note Prefer using the top-level [`openAPI`](https://nitro.build/#openapi) option for configuration. :: #### `typescriptBundlerResolution` Enable TypeScript bundler module resolution. See [TypeScript#51669](https://github.com/microsoft/TypeScript/pull/51669){rel=""nofollow""}. #### `asyncContext` Enable native async context support for `useRequest()`. #### `sourcemapMinify` Set to `false` to disable experimental sourcemap minification. #### `envExpansion` Allow env expansion in runtime config. See [#2043](https://github.com/nitrojs/nitro/pull/2043){rel=""nofollow""}. #### `database` Enable experimental database support. See [Database](https://nitro.build/docs/database). #### `tasks` Enable experimental tasks support. See [Tasks](https://nitro.build/docs/tasks). ```ts export default defineNitroConfig({ experimental: { typescriptBundlerResolution: true, asyncContext: true, envExpansion: true, database: true, tasks: true, }, }); ``` ### `openAPI` Top-level OpenAPI configuration. You can pass an object to modify your OpenAPI specification: ```js openAPI: { meta: { title: 'My Awesome Project', description: 'This might become the next big thing.', version: '1.0' } } ``` These routes are disabled by default in production. To enable them, use the `production` key. `"runtime"` allows middleware usage, and `"prerender"` is the most efficient because the JSON response is constant. ```js openAPI: { // IMPORTANT: make sure to protect OpenAPI routes if necessary! production: "runtime", // or "prerender" } ``` If you like to customize the Scalar integration, you can [pass a configuration object](https://github.com/scalar/scalar){rel=""nofollow""} like this: ```js openAPI: { ui: { scalar: { theme: 'purple' } } } ``` Or if you want to customize the endpoints: ```js openAPI: { route: "/_docs/openapi.json", ui: { scalar: { route: "/_docs/scalar" }, swagger: { route: "/_docs/swagger" } } } ``` ### `future` - Default: `{}` New features pending for a major version to avoid breaking changes. #### `nativeSWR` Uses built-in SWR functionality (using caching layer and storage) for Netlify and Vercel presets instead of falling back to ISR behavior. ```ts export default defineNitroConfig({ future: { nativeSWR: true, }, }); ``` ### `storage` - Default: `{}` Storage configuration, read more in the [Storage Layer](https://nitro.build/docs/storage) section. ```ts export default defineNitroConfig({ storage: { redis: { driver: "redis", url: "redis://localhost:6379", }, }, }); ``` ### `devStorage` - Default: `{}` Storage configuration overrides for development mode. ```ts export default defineNitroConfig({ devStorage: { redis: { driver: "fs", base: "./data/redis", // use filesystem in development }, }, }); ``` ### `database` Database connection configurations. Requires `experimental.database: true`. ```ts database: { default: { connector: "sqlite", options: { name: "db" } } } ``` ### `devDatabase` Database connection configuration overrides for development mode. ```ts export default defineNitroConfig({ devDatabase: { default: { connector: "sqlite", options: { name: "db-dev" }, // separate dev database }, }, }); ``` ### `renderer` - Type: `false`{.shiki,shiki-themes,github-light,github-dark,github-dark lang="ts"} | `{ handler?: string, static?: boolean, template?: string }`{.shiki,shiki-themes,github-light,github-dark,github-dark lang="ts"} Points to main render entry (file should export an event handler as default). ```ts export default defineNitroConfig({ renderer: { handler: "~/renderer", // path to the render handler }, }); ``` ### `serveStatic` - Type: `boolean`{.shiki,shiki-themes,github-light,github-dark,github-dark lang="ts"} | `'node'`{.shiki,shiki-themes,github-light,github-dark,github-dark lang="ts"} | `'deno'`{.shiki,shiki-themes,github-light,github-dark,github-dark lang="ts"} | `'inline'`{.shiki,shiki-themes,github-light,github-dark,github-dark lang="ts"} - Default: depends on the deployment preset used. Serve `public/` assets in production. **Note:** It is highly recommended that your edge CDN (Nginx, Apache, Cloud) serves the `.output/public/` directory instead to enable compression and higher level caching. ```ts export default defineNitroConfig({ serveStatic: "node", // serve static assets using Node.js }); ``` ### `noPublicDir` - Default: `false`{.shiki,shiki-themes,github-light,github-dark,github-dark lang="ts"} If enabled, disables `.output/public` directory creation. Skips copying `public/` dir and also disables pre-rendering. ```ts export default defineNitroConfig({ noPublicDir: true, // skip public directory output }); ``` ### `publicAssets` Public asset directories to serve in development and bundle in production. If a `public/` directory is detected, it will be added by default, but you can add more by yourself too! It's possible to set Cache-Control headers for assets using the `maxAge` option: ```ts publicAssets: [ { baseURL: "images", dir: "public/images", maxAge: 60 * 60 * 24 * 7, // 7 days }, ], ``` The config above generates the following header in the assets under `public/images/` folder: `cache-control: public, max-age=604800, immutable` The `dir` option is where your files live on your file system; the `baseURL` option is the folder they will be accessible from when served/bundled. ### `compressPublicAssets` - Default: `{ gzip: false, brotli: false, zstd: false }`{.shiki,shiki-themes,github-light,github-dark,github-dark lang="ts"} If enabled, Nitro will generate a pre-compressed (gzip, brotli, and/or zstd) version of supported types of public assets and prerendered routes larger than 1024 bytes into the public directory. Default compression levels are used. Using this option you can support zero overhead asset compression without using a CDN. ```ts export default defineNitroConfig({ compressPublicAssets: { gzip: true, brotli: true, // enable gzip and brotli pre-compression }, }); ``` ### `serverAssets` Assets can be accessed in server logic and bundled in production. [Read more](https://nitro.build/docs/assets#server-assets). ```ts export default defineNitroConfig({ serverAssets: [ { baseName: "templates", dir: "./templates", // bundle templates/ as server assets }, ], }); ``` ### `modules` - Default: `[]`{.shiki,shiki-themes,github-light,github-dark,github-dark lang="ts"} An array of Nitro modules. Modules can be a string (path), a module object with a `setup` function, or a function. ```ts export default defineNitroConfig({ modules: [ "./modules/my-module.ts", (nitro) => { nitro.hooks.hook("compiled", () => { /* ... */ }); }, ], }); ``` ### `plugins` - Default: `[]` An array of paths to nitro plugins. They will be executed by order on the first initialization. Note that Nitro auto-registers the plugins in the `plugins/` directory, [learn more](https://nitro.build/docs/plugins). ```ts export default defineNitroConfig({ plugins: [ "~/plugins/my-plugin.ts", ], }); ``` ### `tasks` - Default: `{}` Task definitions. Each key is a task name with a `handler` path and optional `description`. ```ts tasks: { 'db:migrate': { handler: './tasks/db-migrate', description: 'Run database migrations' } } ``` ### `scheduledTasks` - Default: `{}` Map of cron expressions to task name(s). ```ts scheduledTasks: { '0 * * * *': 'cleanup:temp', '*/5 * * * *': ['health:check', 'metrics:collect'] } ``` ### `imports` - Default: `false`{.shiki,shiki-themes,github-light,github-dark,github-dark lang="ts"} Auto import options. Set to an object to enable. See [unimport](https://github.com/unjs/unimport){rel=""nofollow""} for more information. ```ts export default defineNitroConfig({ imports: { dirs: ["./utils"], // auto-import from utils/ directory }, }); ``` ### `virtual` - Default: `{}` A map from dynamic virtual import names to their contents or an (async) function that returns it. ```ts export default defineNitroConfig({ virtual: { "#config": `export default { version: "1.0.0" }`, }, }); ``` ### `ignore` - Default: `[]` Array of glob patterns to ignore when scanning directories. ```ts export default defineNitroConfig({ ignore: [ "routes/_legacy/**", // skip legacy route handlers ], }); ``` ### `wasm` - Default: `{}`{.shiki,shiki-themes,github-light,github-dark,github-dark lang="ts"} - Type: `false`{.shiki,shiki-themes,github-light,github-dark,github-dark lang="ts"} | `UnwasmPluginOptions`{.shiki,shiki-themes,github-light,github-dark,github-dark lang="ts"} WASM support configuration. See [unwasm](https://github.com/unjs/unwasm){rel=""nofollow""} for options. ```ts export default defineNitroConfig({ wasm: {}, // enable WASM import support }); ``` ## Dev ### `devServer` - Default: `{ watch: [] }`{.shiki,shiki-themes,github-light,github-dark,github-dark lang="ts"} Dev server options. You can use `watch` to make the dev server reload if any file changes in specified paths. Supports `port`, `hostname`, `watch`, and `runner` options. ```ts export default defineNitroConfig({ devServer: { port: 3001, watch: ["./server/plugins"], }, }); ``` ### `watchOptions` Watch options for development mode. See [chokidar](https://github.com/paulmillr/chokidar){rel=""nofollow""} for more information. ```ts export default defineNitroConfig({ watchOptions: { ignored: ["**/node_modules/**", "**/dist/**"], }, }); ``` ### `devProxy` Proxy configuration for development server. You can use this option to override development server routes and proxy-pass requests. ```js { devProxy: { '/proxy/test': 'http://localhost:3001', '/proxy/example': { target: 'https://example.com', changeOrigin: true } } } ``` See [httpxy](https://github.com/unjs/httpxy){rel=""nofollow""} for all available target options. ## Logging ### `logging` - Default: `{ compressedSizes: true, buildSuccess: true }`{.shiki,shiki-themes,github-light,github-dark,github-dark lang="ts"} Control build logging behavior. Set `compressedSizes` to `false` to skip reporting compressed bundle sizes. Set `buildSuccess` to `false` to suppress the build success message. ```ts export default defineNitroConfig({ logging: { compressedSizes: false, // skip compressed size reporting buildSuccess: false, }, }); ``` ## Routing ### `baseURL` Default: `/`{.shiki,shiki-themes,github-light,github-dark,github-dark lang="ts"} (or `NITRO_APP_BASE_URL` environment variable if provided) Server's main base URL. ```ts export default defineNitroConfig({ baseURL: "/app/", // serve app under /app/ prefix }); ``` ### `apiBaseURL` - Default: `/api` Changes the default API base URL prefix. ```ts export default defineNitroConfig({ apiBaseURL: "/server/api", // api routes under /server/api/ }); ``` ### `handlers` Server handlers and routes. If `routes/`, `api/` or `middleware/` directories exist inside the server directory, they will be automatically added to the handlers array. ```ts export default defineNitroConfig({ handlers: [ { route: "/health", handler: "./handlers/health.ts" }, { route: "/admin/**", handler: "./handlers/admin.ts", method: "get" }, ], }); ``` ### `devHandlers` Regular handlers refer to the path of handlers to be imported and transformed by the bundler. There are situations in that we directly want to provide a handler instance with programmatic usage. We can use `devHandlers` but note that they are **only available in development mode** and **not in production build**. ```ts export default defineNitroConfig({ devHandlers: [ { route: "/__dev", handler: eventHandler(() => "dev-only route") }, ], }); ``` ### `routes` - Default: `{}`{.shiki,shiki-themes,github-light,github-dark,github-dark lang="ts"} Inline route definitions. A map from route pattern to handler path or handler options. ```ts export default defineNitroConfig({ routes: { "/hello": "./routes/hello.ts", "/greet": { handler: "./routes/greet.ts", method: "post" }, }, }); ``` ### `errorHandler` - Type: `string`{.shiki,shiki-themes,github-light,github-dark,github-dark lang="ts"} | `string[]`{.shiki,shiki-themes,github-light,github-dark,github-dark lang="ts"} Path(s) to custom runtime error handler(s). Replaces nitro's built-in error page. **Example:** ::CodeGroup ```js [nitro.config] import { defineNitroConfig } from "nitro/config"; export default defineNitroConfig({ errorHandler: "~/error", }); ``` ```js [error.ts] export default defineNitroErrorHandler((error, event) => { return new Response('[custom error handler] ' + error.stack, { headers: { 'Content-Type': 'text/plain' } }); }); ``` :: ### `routeRules` **🧪 Experimental!** Route options. It is a map from route pattern (following [rou3](https://github.com/h3js/rou3){rel=""nofollow""}) to route options. When `cache` option is set, handlers matching pattern will be automatically wrapped with `defineCachedEventHandler`. See the [Cache API](https://nitro.build/docs/cache) for all available cache options. ::note `swr: true|number` is shortcut for `cache: { swr: true, maxAge: number }` :: **Example:** ```js 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' }, // uses status code 307 (Temporary Redirect) '/old-page2': { redirect: { to:'/new-page2', statusCode: 301 } }, '/old-page/**': { redirect: '/new-page/**' }, '/proxy/example': { proxy: 'https://example.com' }, '/proxy/**': { proxy: '/api/**' }, '/admin/**': { basicAuth: { username: 'admin', password: 'secret' } }, } ``` ### `prerender` Default: ```ts { autoSubfolderIndex: true, concurrency: 1, interval: 0, failOnError: false, crawlLinks: false, ignore: [], routes: [], retry: 3, retryDelay: 500 } ``` Prerendered options. Any route specified will be fetched during the build and copied to the `.output/public` directory as a static asset. Any route (string) that starts with a prefix listed in `ignore` or matches a regular expression or function will be ignored. If `crawlLinks` option is set to `true`, nitro starts with `/` by default (or all routes in `routes` array) and for HTML pages extracts `` tags and prerender them as well. You can set `failOnError` option to `true` to stop the CI when Nitro could not prerender a route. The `interval` and `concurrency` options lets you control the speed of pre-rendering, can be useful to avoid hitting some rate-limit if you call external APIs. Set `autoSubfolderIndex` lets you control how to generate the files in the `.output/public` directory: ```bash # autoSubfolderIndex: true (default) /about -> .output/public/about/index.html # autoSubfolderIndex: false /about -> .output/public/about.html ``` This option is useful when your hosting provider does not give you an option regarding the trailing slash. The prerenderer will attempt to render pages 3 times with a delay of 500ms. Use `retry` and `retryDelay` to change this behavior. ## Directories ### `workspaceDir` Project workspace root directory. The workspace (e.g. pnpm workspace) directory is automatically detected when the `workspaceDir` option is not set. ```ts export default defineNitroConfig({ workspaceDir: "../", // monorepo root }); ``` ### `rootDir` Project main directory. ```ts export default defineNitroConfig({ rootDir: "./src/server", }); ``` ### `serverDir` - Default: `false`{.shiki,shiki-themes,github-light,github-dark,github-dark lang="ts"} - Type: `boolean`{.shiki,shiki-themes,github-light,github-dark,github-dark lang="ts"} | `"./"` | `"./server"` | `string`{.shiki,shiki-themes,github-light,github-dark,github-dark lang="ts"} Server directory for scanning `api/`, `routes/`, `plugins/`, `utils/`, `middleware/`, `assets/`, and `tasks/` folders. When set to `false`, automatic directory scanning is disabled. Set to `"./"` to use the root directory, or `"./server"` to use a `server/` subdirectory. ```ts export default defineNitroConfig({ serverDir: "./server", // scan server/ subdirectory }); ``` ### `scanDirs` - Default: (source directory when empty array) List of directories to scan and auto-register files, such as API routes. ```ts export default defineNitroConfig({ scanDirs: ["./modules/auth/api", "./modules/billing/api"], }); ``` ### `apiDir` - Default: `api` Defines a different directory to scan for api route handlers. ```ts export default defineNitroConfig({ apiDir: "endpoints", // scan endpoints/ instead of api/ }); ``` ### `routesDir` - Default: `routes` Defines a different directory to scan for route handlers. ```ts export default defineNitroConfig({ routesDir: "pages", // scan pages/ instead of routes/ }); ``` ### `buildDir` - Default: `node_modules/.nitro` Nitro's temporary working directory for generating build-related files. ```ts export default defineNitroConfig({ buildDir: ".nitro", // use .nitro/ in project root }); ``` ### `output` - Default: `{ dir: '.output', serverDir: '.output/server', publicDir: '.output/public' }` Output directories for production bundle. ```ts export default defineNitroConfig({ output: { dir: "dist", serverDir: "dist/server", publicDir: "dist/public", }, }); ``` ## Build ### `builder` - Type: `"rollup"`{.shiki,shiki-themes,github-light,github-dark,github-dark lang="ts"} | `"rolldown"`{.shiki,shiki-themes,github-light,github-dark,github-dark lang="ts"} | `"vite"`{.shiki,shiki-themes,github-light,github-dark,github-dark lang="ts"} - Default: `undefined`{.shiki,shiki-themes,github-light,github-dark,github-dark lang="ts"} (auto-detected) Specify the bundler to use for building. ```ts export default defineNitroConfig({ builder: "vite", }); ``` ### `rollupConfig` Additional rollup configuration. ```ts export default defineNitroConfig({ rollupConfig: { output: { manualChunks: { vendor: ["lodash-es"] } }, }, }); ``` ### `rolldownConfig` Additional rolldown configuration. ```ts export default defineNitroConfig({ rolldownConfig: { output: { banner: "/* built with nitro */" }, }, }); ``` ### `entry` Bundler entry point. ```ts export default defineNitroConfig({ entry: "./server/entry.ts", // custom entry file }); ``` ### `unenv` [unenv](https://github.com/unjs/unenv/){rel=""nofollow""} preset(s) for environment compatibility. ```ts export default defineNitroConfig({ unenv: { alias: { "my-module": "my-module/web" }, }, }); ``` ### `alias` Path aliases for module resolution. ```ts export default defineNitroConfig({ alias: { "~utils": "./src/utils", "#shared": "./shared", }, }); ``` ### `minify` - Default: `false` Minify bundle. ```ts export default defineNitroConfig({ minify: true, // minify production bundle }); ``` ### `inlineDynamicImports` - Default: `false` Bundle all code into a single file instead of creating separate chunks per route. When `false`, each route handler becomes a separate chunk loaded on-demand. When `true`, everything is bundled together. Some presets enable this by default. ```ts export default defineNitroConfig({ inlineDynamicImports: true, // single output file }); ``` ### `sourcemap` - Default: `false`{.shiki,shiki-themes,github-light,github-dark,github-dark lang="ts"} Enable source map generation. See [options](https://rollupjs.org/configuration-options/#output-sourcemap){rel=""nofollow""}. ```ts export default defineNitroConfig({ sourcemap: true, // generate .map files }); ``` ### `node` - Default: `true`{.shiki,shiki-themes,github-light,github-dark,github-dark lang="ts"} Specify whether the build is used for Node.js or not. If set to `false`, nitro tries to mock Node.js dependencies using [unenv](https://github.com/unjs/unenv){rel=""nofollow""} and adjust its behavior. ```ts export default defineNitroConfig({ node: false, // target non-Node.js runtimes }); ``` ### `moduleSideEffects` Default: `['unenv/polyfill/']` Specifies module imports that have side-effects. ```ts export default defineNitroConfig({ moduleSideEffects: ["unenv/polyfill/", "reflect-metadata"], }); ``` ### `replace` Build-time string replacements. ```ts export default defineNitroConfig({ replace: { "process.env.APP_VERSION": JSON.stringify("1.0.0"), }, }); ``` ### `commonJS` Specifies additional configuration for the rollup CommonJS plugin. ```ts export default defineNitroConfig({ commonJS: { requireReturnsDefault: "auto", }, }); ``` ### `exportConditions` Custom export conditions for module resolution. ```ts export default defineNitroConfig({ exportConditions: ["worker", "production"], }); ``` ### `noExternals` - Default: `false`{.shiki,shiki-themes,github-light,github-dark,github-dark lang="ts"} Prevent specific packages from being externalized. Set to `true` to bundle all dependencies, or pass an array of package names/patterns. ```ts export default defineNitroConfig({ noExternals: true, // bundle all dependencies }); ``` ### `traceDeps` - Default: `[]`{.shiki,shiki-themes,github-light,github-dark,github-dark lang="ts"} Additional dependencies to trace and include in the build output. ```ts export default defineNitroConfig({ traceDeps: ["sharp", "better-sqlite3"], }); ``` ### `oxc` OXC options for rolldown builds. Includes `minify` and `transform` sub-options. ```ts export default defineNitroConfig({ oxc: { minify: { compress: true, mangle: true }, }, }); ``` ## Advanced ### `dev` - Default: `true` for development and `false` for production. **⚠️ Caution! This is an advanced configuration. Things can go wrong if misconfigured.** ```ts export default defineNitroConfig({ dev: true, // force development mode behavior }); ``` ### `typescript` Default: `{ strict: true, generateRuntimeConfigTypes: false, generateTsConfig: false }`{.shiki,shiki-themes,github-light,github-dark,github-dark lang="ts"} TypeScript configuration options including `strict`, `generateRuntimeConfigTypes`, `generateTsConfig`, `tsConfig`, `generatedTypesDir`, and `tsconfigPath`. ```ts export default defineNitroConfig({ typescript: { strict: true, generateTsConfig: true, }, }); ``` ### `hooks` **⚠️ Caution! This is an advanced configuration. Things can go wrong if misconfigured.** nitro hooks. See [hookable](https://github.com/unjs/hookable){rel=""nofollow""} for more information. ```ts export default defineNitroConfig({ hooks: { compiled(nitro) { console.log("Build compiled successfully!"); }, }, }); ``` ### `commands` **⚠️ Caution! This is an advanced configuration. Things can go wrong if misconfigured.** Preview and deploy command hints are usually filled by deployment presets. ```ts export default defineNitroConfig({ commands: { preview: "node ./server/index.mjs", }, }); ``` ### `devErrorHandler` **⚠️ Caution! This is an advanced configuration. Things can go wrong if misconfigured.** A custom error handler function for development errors. ```ts export default defineNitroConfig({ devErrorHandler: (error, event) => { return new Response(`Dev error: ${error.message}`, { status: 500 }); }, }); ``` ### `framework` - Default: `{ name: "nitro", version: "" }`{.shiki,shiki-themes,github-light,github-dark,github-dark lang="ts"} Framework information. Used by presets and build info. Typically set by higher-level frameworks (e.g. Nuxt). ```ts export default defineNitroConfig({ framework: { name: "my-framework", version: "2.0.0" }, }); ``` ## Preset options ### `firebase` The options for the firebase functions preset. See [Preset Docs](https://nitro.build/deploy/providers/firebase#options) ```ts export default defineNitroConfig({ firebase: { gen: 2, // use Cloud Functions 2nd gen region: "us-central1", }, }); ``` ### `vercel` The options for the vercel preset. See [Preset Docs](https://nitro.build/deploy/providers/vercel) ```ts export default defineNitroConfig({ vercel: { config: { runtime: "nodejs20.x" }, }, }); ``` ### `cloudflare` The options for the cloudflare preset. See [Preset Docs](https://nitro.build/deploy/providers/cloudflare) ```ts export default defineNitroConfig({ cloudflare: { wrangler: { compatibility_date: "2025-01-01" }, }, }); ``` ### `zephyr` The options for the zephyr preset. See [Preset Docs](https://nitro.build/deploy/providers/zephyr#options) # Examples # API Routes ::code-tree{expand-all default-value="api/hello.ts"} ```html [index.html] API Routes

API Routes:

``` ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ serverDir: "./", }); ``` ```json [package.json] { "type": "module", "scripts": { "dev": "nitro dev", "build": "nitro build" }, "devDependencies": { "nitro": "latest" } } ``` ```json [tsconfig.json] { "extends": "nitro/tsconfig" } ``` ```ts [vite.config.ts] import { defineConfig } from "vite"; import { nitro } from "nitro/vite"; export default defineConfig({ plugins: [nitro()] }); ``` ```ts [api/hello.ts] import { defineHandler } from "nitro"; export default defineHandler(() => "Nitro is amazing!"); ``` ```ts [api/test.get.ts] import { defineHandler } from "nitro"; export default defineHandler(() => "Test get handler"); ``` ```ts [api/test.post.ts] import { defineHandler } from "nitro"; export default defineHandler(async (event) => { const body = await event.req.json(); return { message: "Test post handler", body, }; }); ``` ```ts [api/hello/[name\\].ts] import { defineHandler } from "nitro"; export default defineHandler((event) => `Hello (param: ${event.context.params!.name})!`); ``` :: Nitro supports file-based routing in the `api/` or `routes/` directory. Each file becomes an API endpoint based on its path. ## Basic Route Create a file in the `api/` directory to define a route. The file path becomes the URL path: ```ts [api/hello.ts] import { defineHandler } from "nitro"; export default defineHandler(() => "Nitro is amazing!"); ``` This creates a `GET /api/hello` endpoint. ## Dynamic Routes Use square brackets `[param]` for dynamic URL segments. Access params via `event.context.params`: ```ts [api/hello/[name\\].ts] import { defineHandler } from "nitro"; export default defineHandler((event) => `Hello (param: ${event.context.params!.name})!`); ``` This creates a `GET /api/hello/:name` endpoint (e.g., `/api/hello/world`). ## HTTP Methods Suffix your file with the HTTP method (`.get.ts`, `.post.ts`, `.put.ts`, `.delete.ts`, etc.): ### GET Handler ```ts [api/test.get.ts] import { defineHandler } from "nitro"; export default defineHandler(() => "Test get handler"); ``` ### POST Handler ```ts [api/test.post.ts] import { defineHandler } from "nitro"; export default defineHandler(async (event) => { const body = await event.req.json(); return { message: "Test post handler", body, }; }); ``` ## Learn More - [Routing](https://nitro.build/docs/routing) # Auto Imports ::code-tree{expand-all default-value="nitro.config.ts"} ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ serverDir: true, imports: {}, }); ``` ```json [package.json] { "type": "module", "scripts": { "dev": "nitro dev", "build": "nitro build" }, "devDependencies": { "nitro": "latest" } } ``` ```ts [server.ts] import { defineHandler } from "nitro"; import { makeGreeting } from "./server/utils/hello.ts"; export default defineHandler(() => `

${makeGreeting("Nitro")}

`); ``` ```json [tsconfig.json] { "include": [".nitro/types/nitro-imports.d.ts", "src"] } ``` ```ts [vite.config.ts] import { defineConfig } from "vite"; import { nitro } from "nitro/vite"; export default defineConfig({ plugins: [nitro()] }); ``` ```ts [server/utils/hello.ts] export function makeGreeting(name: string) { return `Hello, ${name}!`; } ``` :: Functions exported from `server/utils/` are automatically available without explicit imports when auto-imports are enabled. Define a utility once and use it anywhere in your server code. ## Configuration Enable auto-imports by setting `imports` in your config: ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ serverDir: true, imports: {}, }); ``` ## Using Auto Imports 1. Create a utility file in `server/utils/`: ```ts [server/utils/hello.ts] export function makeGreeting(name: string) { return `Hello, ${name}!`; } ``` 2. The function is available without importing it: ```ts [server.ts] import { defineHandler } from "nitro"; import { makeGreeting } from "./server/utils/hello.ts"; export default defineHandler(() => `

${makeGreeting("Nitro")}

`); ``` With this setup, any function exported from `server/utils/` becomes globally available. Nitro scans the directory and generates the necessary imports automatically. ## Learn More - [Configuration](https://nitro.build/docs/configuration) # Cached Handler ::code-tree{expand-all default-value="server.ts"} ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({}); ``` ```json [package.json] { "type": "module", "scripts": { "dev": "nitro dev", "build": "nitro build" }, "devDependencies": { "nitro": "latest" } } ``` ```ts [server.ts] import { html } from "nitro"; import { defineCachedHandler } from "nitro/cache"; export default defineCachedHandler( async () => { await new Promise((resolve) => setTimeout(resolve, 500)); return html` Response generated at ${new Date().toISOString()} (took 500ms)
(skip cache) `; }, { shouldBypassCache: ({ req }) => req.url.includes("skipCache=true") } ); ``` ```json [tsconfig.json] { "extends": "nitro/tsconfig" } ``` ```ts [vite.config.ts] import { defineConfig } from "vite"; import { nitro } from "nitro/vite"; export default defineConfig({ plugins: [nitro()] }); ``` :: This example shows how to cache an expensive operation (a 500 ms delay) and conditionally bypass the cache using a query parameter. On first request, the handler executes and caches the result. Subsequent requests return the cached response instantly until the cache expires or is bypassed. ## How It Works ```ts [server.ts] import { html } from "nitro"; import { defineCachedHandler } from "nitro/cache"; export default defineCachedHandler( async () => { await new Promise((resolve) => setTimeout(resolve, 500)); return html` Response generated at ${new Date().toISOString()} (took 500ms)
(skip cache) `; }, { shouldBypassCache: ({ req }) => req.url.includes("skipCache=true") } ); ``` The handler simulates a slow operation with a 500ms delay. As `defineCachedHandler` wraps it, the response is cached after the first execution. The `shouldBypassCache` option checks for `?skipCache=true` in the URL and when present the cache is skipped and the handler runs fresh. ## Learn More - [Cache](https://nitro.build/docs/cache) - [Storage](https://nitro.build/docs/storage) # Custom Error Handler ::code-tree{expand-all default-value="error.ts"} ```ts [error.ts] import { defineErrorHandler } from "nitro"; export default defineErrorHandler((error, _event) => { return new Response(`Custom Error Handler: ${error.message}`, { status: 500, headers: { "Content-Type": "text/plain" }, }); }); ``` ```ts [nitro.config.ts] import { defineConfig } from "nitro"; // import errorHandler from "./error"; export default defineConfig({ errorHandler: "./error.ts", // devErrorHandler: errorHandler, }); ``` ```json [package.json] { "type": "module", "scripts": { "dev": "nitro dev", "build": "nitro build" }, "devDependencies": { "nitro": "latest" } } ``` ```ts [server.ts] import { defineHandler, HTTPError } from "nitro"; export default defineHandler(() => { throw new HTTPError("Example Error!", { status: 500 }); }); ``` ```json [tsconfig.json] { "extends": "nitro/tsconfig" } ``` ```ts [vite.config.ts] import { defineConfig } from "vite"; import { nitro } from "nitro/vite"; export default defineConfig({ plugins: [nitro()] }); ``` :: This example shows how to intercept all errors and return a custom response format. When any route throws an error, Nitro calls your error handler instead of returning the default error page. ## Error Handler Create an `error.ts` file in your project root to define the global error handler: ```ts [error.ts] import { defineErrorHandler } from "nitro"; export default defineErrorHandler((error, _event) => { return new Response(`Custom Error Handler: ${error.message}`, { status: 500, headers: { "Content-Type": "text/plain" }, }); }); ``` The handler receives the thrown error and the H3 event object. You can use the event to access request details like headers, cookies, or the URL path to customize responses per route. ## Triggering an Error The main handler throws an error to demonstrate the custom error handler: ```ts [server.ts] import { defineHandler, HTTPError } from "nitro"; export default defineHandler(() => { throw new HTTPError("Example Error!", { status: 500 }); }); ``` When you visit the page, instead of seeing a generic error page, you'll see "Custom Error Handler: Example Error!" because the error handler intercepts the thrown error. ## Learn More - [Server Entry](https://nitro.build/docs/server-entry) # Database ::code-tree{expand-all default-value="server.ts"} ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ experimental: { database: true, tasks: true, }, database: { default: { connector: "sqlite" }, }, }); ``` ```json [package.json] { "type": "module", "scripts": { "dev": "nitro dev", "build": "nitro build" }, "devDependencies": { "nitro": "latest" } } ``` ```ts [server.ts] import { defineHandler } from "nitro"; import { useDatabase } from "nitro/database"; export default defineHandler(async () => { const db = useDatabase(); // Create users table await db.sql`DROP TABLE IF EXISTS users`; await db.sql`CREATE TABLE IF NOT EXISTS users ("id" TEXT PRIMARY KEY, "firstName" TEXT, "lastName" TEXT, "email" TEXT)`; // Add a new user const userId = String(Math.round(Math.random() * 10_000)); await db.sql`INSERT INTO users VALUES (${userId}, 'John', 'Doe', '')`; // Query for users const { rows } = await db.sql`SELECT * FROM users WHERE id = ${userId}`; return { rows, }; }); ``` ```json [tsconfig.json] { "extends": "nitro/tsconfig" } ``` ```ts [vite.config.ts] import { defineConfig } from "vite"; import { nitro } from "nitro/vite"; export default defineConfig({ plugins: [nitro()] }); ``` ```ts [tasks/db/migrate.ts] import { defineTask } from "nitro/task"; import { useDatabase } from "nitro/database"; export default defineTask({ meta: { description: "Run database migrations", }, async run() { const db = useDatabase(); console.log("Running database migrations..."); // Create users table await db.sql`DROP TABLE IF EXISTS users`; await db.sql`CREATE TABLE IF NOT EXISTS users ("id" TEXT PRIMARY KEY, "firstName" TEXT, "lastName" TEXT, "email" TEXT)`; return { result: "Database migrations complete!", }; }, }); ``` :: Nitro provides a built-in database layer that uses SQL template literals for safe, parameterized queries. This example creates a users table, inserts a record, and queries it back. ## Querying the Database ```ts [server.ts] import { defineHandler } from "nitro"; import { useDatabase } from "nitro/database"; export default defineHandler(async () => { const db = useDatabase(); // Create users table await db.sql`DROP TABLE IF EXISTS users`; await db.sql`CREATE TABLE IF NOT EXISTS users ("id" TEXT PRIMARY KEY, "firstName" TEXT, "lastName" TEXT, "email" TEXT)`; // Add a new user const userId = String(Math.round(Math.random() * 10_000)); await db.sql`INSERT INTO users VALUES (${userId}, 'John', 'Doe', '')`; // Query for users const { rows } = await db.sql`SELECT * FROM users WHERE id = ${userId}`; return { rows, }; }); ``` Retrieve the database instance using `useDatabase()`. The database can be queried using `db.sql`, and variables like `${userId}` are automatically escaped to prevent SQL injection. ## Running Migrations with Tasks Nitro tasks let you run operations outside of request handlers. For database migrations, create a task file in `tasks/` and run it via the CLI. This keeps schema changes separate from your application code. ```ts [tasks/db/migrate.ts] import { defineTask } from "nitro/task"; import { useDatabase } from "nitro/database"; export default defineTask({ meta: { description: "Run database migrations", }, async run() { const db = useDatabase(); console.log("Running database migrations..."); // Create users table await db.sql`DROP TABLE IF EXISTS users`; await db.sql`CREATE TABLE IF NOT EXISTS users ("id" TEXT PRIMARY KEY, "firstName" TEXT, "lastName" TEXT, "email" TEXT)`; return { result: "Database migrations complete!", }; }, }); ``` ## Learn More - [Database](https://nitro.build/docs/database) - [Tasks](https://nitro.build/docs/tasks) # Elysia ::code-tree{expand-all default-value="server.ts"} ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({}); ``` ```json [package.json] { "type": "module", "scripts": { "build": "nitro build", "dev": "nitro dev" }, "devDependencies": { "elysia": "^1.4.22", "nitro": "latest" } } ``` ```ts [server.ts] import { Elysia } from "elysia"; const app = new Elysia(); app.get("/", () => "Hello, Elysia with Nitro!"); export default app.compile(); ``` ```json [tsconfig.json] { "extends": "nitro/tsconfig" } ``` ```ts [vite.config.ts] import { defineConfig } from "vite"; import { nitro } from "nitro/vite"; export default defineConfig({ plugins: [nitro()] }); ``` :: ## Server Entry ```ts [server.ts] import { Elysia } from "elysia"; const app = new Elysia(); app.get("/", () => "Hello, Elysia with Nitro!"); export default app.compile(); ``` Nitro auto-detects `server.ts` in your project root and uses it as the server entry. The Elysia app handles all incoming requests, giving you full control over routing and middleware. Call `app.compile()` before exporting to optimize the router for production. ## Learn More - [Server Entry](https://nitro.build/docs/server-entry) - [Elysia Documentation](https://elysiajs.com/){rel=""nofollow""} # Express ::code-tree{expand-all default-value="server.node.ts"} ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({}); ``` ```json [package.json] { "type": "module", "scripts": { "build": "nitro build", "dev": "nitro dev" }, "devDependencies": { "@types/express": "^5.0.6", "express": "^5.2.1", "nitro": "latest" } } ``` ```ts [server.node.ts] import Express from "express"; const app = Express(); app.use("/", (_req, res) => { res.send("Hello from Express with Nitro!"); }); export default app; ``` ```json [tsconfig.json] { "extends": "nitro/tsconfig" } ``` ```ts [vite.config.ts] import { defineConfig } from "vite"; import { nitro } from "nitro/vite"; export default defineConfig({ plugins: [nitro()] }); ``` :: ## Server Entry ```ts [server.node.ts] import Express from "express"; const app = Express(); app.use("/", (_req, res) => { res.send("Hello from Express with Nitro!"); }); export default app; ``` Nitro auto-detects `server.node.ts` in your project root and uses it as the server entry. The Express app handles all incoming requests, giving you full control over routing and middleware. ::note The `.node.ts` suffix indicates this entry is Node.js specific and won't work in other runtimes like Cloudflare Workers or Deno. :: ## Learn More - [Server Entry](https://nitro.build/docs/server-entry) - [Express Documentation](https://expressjs.com/){rel=""nofollow""} # Fastify ::code-tree{expand-all default-value="server.node.ts"} ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({}); ``` ```json [package.json] { "type": "module", "scripts": { "build": "nitro build", "dev": "nitro dev" }, "devDependencies": { "fastify": "^5.7.4", "nitro": "latest" } } ``` ```ts [server.node.ts] import Fastify from "fastify"; const app = Fastify(); app.get("/", () => "Hello, Fastify with Nitro!"); await app.ready(); export default app.routing; ``` ```json [tsconfig.json] { "extends": "nitro/tsconfig" } ``` ```ts [vite.config.ts] import { defineConfig } from "vite"; import { nitro } from "nitro/vite"; export default defineConfig({ plugins: [nitro()] }); ``` :: ## Server Entry ```ts [server.node.ts] import Fastify from "fastify"; const app = Fastify(); app.get("/", () => "Hello, Fastify with Nitro!"); await app.ready(); export default app.routing; ``` Nitro auto-detects `server.node.ts` in your project root and uses it as the server entry. Call `await app.ready()` to initialize all registered plugins before exporting. Export `app.routing` (not `app`) to provide Nitro with the request handler function. ::note The `.node.ts` suffix indicates this entry is Node.js specific and won't work in other runtimes like Cloudflare Workers or Deno. :: ## Learn More - [Server Entry](https://nitro.build/docs/server-entry) - [Fastify Documentation](https://fastify.dev/){rel=""nofollow""} # Hello World ::code-tree{expand-all default-value="server.ts"} ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({}); ``` ```json [package.json] { "type": "module", "scripts": { "build": "nitro build", "dev": "nitro dev", "preview": "node .output/server/index.mjs" }, "devDependencies": { "nitro": "latest" } } ``` ```ts [server.ts] export default { fetch(req: Request) { return new Response("Nitro Works!"); }, }; ``` ```json [tsconfig.json] { "extends": "nitro/tsconfig" } ``` ```ts [vite.config.ts] import { defineConfig } from "vite"; import { nitro } from "nitro/vite"; export default defineConfig({ plugins: [nitro()] }); ``` :: The simplest Nitro server. Export an object with a `fetch` method that receives a standard `Request` and returns a `Response`. No frameworks, no abstractions, just the web platform. ## Server Entry ```ts [server.ts] export default { fetch(req: Request) { return new Response("Nitro Works!"); }, }; ``` The `fetch` method follows the same signature as Service Workers and Cloudflare Workers. This pattern works across all deployment targets because it uses web standards. Add the Nitro plugin to Vite and it handles the rest: dev server, hot reloading, and production builds. ## Learn More - [Server Entry](https://nitro.build/docs/server-entry) - [Configuration](https://nitro.build/docs/configuration) # Hono ::code-tree{expand-all default-value="server.ts"} ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({}); ``` ```json [package.json] { "type": "module", "scripts": { "build": "nitro build", "dev": "nitro dev" }, "devDependencies": { "hono": "^4.11.8", "nitro": "latest" } } ``` ```ts [server.ts] import { Hono } from "hono"; const app = new Hono(); app.get("/", (c) => { return c.text("Hello, Hono with Nitro!"); }); export default app; ``` ```json [tsconfig.json] { "extends": "nitro/tsconfig" } ``` ```ts [vite.config.ts] import { defineConfig } from "vite"; import { nitro } from "nitro/vite"; export default defineConfig({ plugins: [nitro()] }); ``` :: ## Server Entry ```ts [server.ts] import { Hono } from "hono"; const app = new Hono(); app.get("/", (c) => { return c.text("Hello, Hono with Nitro!"); }); export default app; ``` Nitro auto-detects `server.ts` in your project root and uses it as the server entry. The Hono app handles all incoming requests, giving you full control over routing and middleware. Hono is cross-runtime compatible, so this server entry works across all Nitro deployment targets including Node.js, Deno, Bun, and Cloudflare Workers. ## Learn More - [Server Entry](https://nitro.build/docs/server-entry) - [Hono Documentation](https://hono.dev/){rel=""nofollow""} # Import Alias ::code-tree{expand-all default-value="server/routes/index.ts"} ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ serverDir: true, }); ``` ```json [package.json] { "type": "module", "imports": { "#server/*": "./server/*" }, "scripts": { "build": "nitro build", "dev": "nitro dev", "preview": "node .output/server/index.mjs" }, "devDependencies": { "nitro": "latest" } } ``` ```json [tsconfig.json] { "extends": "nitro/tsconfig", "compilerOptions": { "paths": { "~server/*": ["./server/*"] } } } ``` ```ts [vite.config.ts] import { defineConfig } from "vite"; import { nitro } from "nitro/vite"; export default defineConfig({ plugins: [nitro()], resolve: { tsconfigPaths: true } }); ``` ```ts [server/routes/index.ts] import { sum } from "~server/utils/math.ts"; import { rand } from "#server/utils/math.ts"; export default () => { const [a, b] = [rand(1, 10), rand(1, 10)]; const result = sum(a, b); return `The sum of ${a} + ${b} = ${result}`; }; ``` ```ts [server/utils/math.ts] export function rand(min: number, max: number): number { return Math.floor(Math.random() * (max - min + 1)) + min; } export function sum(a: number, b: number): number { return a + b; } ``` :: Import aliases like `~` and `#` let you reference modules with shorter paths instead of relative imports. ## Importing Using Aliases ```ts [server/routes/index.ts] import { sum } from "~server/utils/math.ts"; import { rand } from "#server/utils/math.ts"; export default () => { const [a, b] = [rand(1, 10), rand(1, 10)]; const result = sum(a, b); return `The sum of ${a} + ${b} = ${result}`; }; ``` The route imports the `sum` function using `~server/` and `rand` using `#server/`. Both resolve to the same `server/utils/math.ts` file. The handler generates two random numbers and returns their sum. ## Configuration Aliases can be configured in `package.json` imports field or `nitro.config.ts`. ## Learn More - [Configuration](https://nitro.build/docs/configuration) # Middleware ::code-tree{expand-all default-value="server/middleware/auth.ts"} ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ serverDir: true, }); ``` ```json [package.json] { "type": "module", "scripts": { "dev": "nitro dev", "build": "nitro build" }, "devDependencies": { "nitro": "latest" } } ``` ```ts [server.ts] import { defineHandler } from "nitro"; export default defineHandler((event) => ({ auth: event.context.auth, })); ``` ```json [tsconfig.json] { "extends": "nitro/tsconfig" } ``` ```ts [vite.config.ts] import { defineConfig } from "vite"; import { nitro } from "nitro/vite"; export default defineConfig({ plugins: [nitro()] }); ``` ```ts [server/middleware/auth.ts] import { defineMiddleware } from "nitro"; export default defineMiddleware((event) => { event.context.auth = { name: "User " + Math.round(Math.random() * 100) }; }); ``` :: Middleware functions run before route handlers on every request. They can modify the request, add context, or return early responses. ## Defining Middleware Create files in `server/middleware/`. They run in alphabetical order: ```ts [server/middleware/auth.ts] import { defineMiddleware } from "nitro"; export default defineMiddleware((event) => { event.context.auth = { name: "User " + Math.round(Math.random() * 100) }; }); ``` Middleware can: - Add data to `event.context` for use in handlers - Return a response early to short-circuit the request - Modify request headers or other properties ## Accessing Context in Handlers Data added to `event.context` in middleware is available in all subsequent handlers: ```ts [server.ts] import { defineHandler } from "nitro"; export default defineHandler((event) => ({ auth: event.context.auth, })); ``` ## Learn More - [Routing](https://nitro.build/docs/routing) # Mono JSX ::code-tree{expand-all default-value="server.tsx"} ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({}); ``` ```json [package.json] { "type": "module", "scripts": { "dev": "nitro dev", "build": "nitro build" }, "devDependencies": { "mono-jsx": "latest", "nitro": "latest" } } ``` ```tsx [server.tsx] export default () => (

Nitro + mongo-jsx works!

); ``` ```json [tsconfig.json] { "extends": "nitro/tsconfig", "compilerOptions": { "jsx": "react-jsx", "jsxImportSource": "mono-jsx" } } ``` ```ts [vite.config.ts] import { defineConfig } from "vite"; import { nitro } from "nitro/vite"; export default defineConfig({ plugins: [nitro()] }); ``` :: ## Server Entry ```tsx [server.tsx] export default () => (

Nitro + mongo-jsx works!

); ``` Nitro auto-detects `server.tsx` and uses mono-jsx to transform JSX into HTML. Export a function that returns JSX, and Nitro sends the rendered HTML as the response. ## Learn More - [Renderer](https://nitro.build/docs/renderer) - [mono-jsx](https://github.com/aspect-dev/mono-jsx){rel=""nofollow""} # Nano JSX ::code-tree{expand-all default-value="server.tsx"} ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({}); ``` ```json [package.json] { "type": "module", "scripts": { "dev": "nitro dev", "build": "nitro build" }, "devDependencies": { "nano-jsx": "^0.2.1", "nitro": "latest" } } ``` ```tsx [server.tsx] import { defineHandler, html } from "nitro"; import { renderSSR } from "nano-jsx"; export default defineHandler(() => { return html(renderSSR(() =>

Nitro + nano-jsx works!

)); }); ``` ```json [tsconfig.json] { "extends": "nitro/tsconfig", "compilerOptions": { "jsx": "react-jsx", "jsxImportSource": "nano-jsx/esm" } } ``` ```ts [vite.config.ts] import { defineConfig } from "vite"; import { nitro } from "nitro/vite"; export default defineConfig({ plugins: [nitro()] }); ``` :: ## Server Entry ```tsx [server.tsx] import { defineHandler, html } from "nitro"; import { renderSSR } from "nano-jsx"; export default defineHandler(() => { return html(renderSSR(() =>

Nitro + nano-jsx works!

)); }); ``` Nitro auto-detects `server.tsx` and uses it as the server entry. Use `renderSSR` from nano-jsx to convert JSX into an HTML string. The `html` helper from H3 sets the correct content type header. ## Learn More - [Renderer](https://nitro.build/docs/renderer) - [nano-jsx](https://nanojsx.io/){rel=""nofollow""} # Plugins ::code-tree{expand-all default-value="server/plugins/test.ts"} ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ serverDir: true, }); ``` ```json [package.json] { "type": "module", "scripts": { "dev": "nitro dev", "build": "nitro build" }, "devDependencies": { "nitro": "latest" } } ``` ```ts [server.ts] import { eventHandler } from "h3"; export default eventHandler(() => "

Hello Nitro!

"); ``` ```json [tsconfig.json] { "extends": "nitro/tsconfig" } ``` ```ts [vite.config.ts] import { defineConfig } from "vite"; import { nitro } from "nitro/vite"; export default defineConfig({ plugins: [nitro()] }); ``` ```ts [server/plugins/test.ts] import { definePlugin } from "nitro"; import { useNitroHooks } from "nitro/app"; export default definePlugin((nitroApp) => { const hooks = useNitroHooks(); hooks.hook("response", (event) => { event.headers.set("content-type", "html; charset=utf-8"); }); }); ``` :: Plugins let you hook into Nitro's runtime lifecycle. This example shows a plugin that modifies the `Content-Type` header on every response. Create files in `server/plugins/` and they're automatically loaded at startup. ## Defining a Plugin ```ts [server/plugins/test.ts] import { definePlugin } from "nitro"; import { useNitroHooks } from "nitro/app"; export default definePlugin((nitroApp) => { const hooks = useNitroHooks(); hooks.hook("response", (event) => { event.headers.set("content-type", "html; charset=utf-8"); }); }); ``` The plugin uses `useNitroHooks()` to access the hooks system, then registers a `response` hook that runs after every request. Here it sets the content type to HTML, but you could log requests, add security headers, or modify responses in any way. ## Main Handler ```ts [server.ts] import { eventHandler } from "h3"; export default eventHandler(() => "

Hello Nitro!

"); ``` The handler returns HTML without setting a content type. The plugin automatically adds the correct `Content-Type: html; charset=utf-8` header to the response. ## Learn More - [Plugins](https://nitro.build/docs/plugins) - [Lifecycle](https://nitro.build/docs/lifecycle) # Custom Renderer ::code-tree{expand-all default-value="renderer.ts"} ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ serverDir: "./", renderer: { handler: "./renderer" }, }); ``` ```json [package.json] { "type": "module", "scripts": { "dev": "nitro dev", "build": "nitro build" }, "devDependencies": { "nitro": "latest" } } ``` ```ts [renderer.ts] import { fetch } from "nitro"; export default async function renderer({ url }: { req: Request; url: URL }) { const apiRes = await fetch("/api/hello").then((res) => res.text()); return new Response( /* html */ ` Custom Renderer

Hello from custom renderer!

Current path: ${url.pathname}

API says: ${apiRes}

`, { headers: { "content-type": "text/html; charset=utf-8" } } ); } ``` ```json [tsconfig.json] { "extends": "nitro/tsconfig" } ``` ```ts [vite.config.ts] import { defineConfig } from "vite"; import { nitro } from "nitro/vite"; export default defineConfig({ plugins: [nitro()] }); ``` ```ts [api/hello.ts] import { defineHandler } from "nitro"; export default defineHandler(() => "Nitro is amazing!"); ``` :: Create a custom renderer that generates HTML responses with data from API routes. Use Nitro's internal `fetch` to call routes without network overhead. ## Renderer ```ts [renderer.ts] import { fetch } from "nitro"; export default async function renderer({ url }: { req: Request; url: URL }) { const apiRes = await fetch("/api/hello").then((res) => res.text()); return new Response( /* html */ ` Custom Renderer

Hello from custom renderer!

Current path: ${url.pathname}

API says: ${apiRes}

`, { headers: { "content-type": "text/html; charset=utf-8" } } ); } ``` Nitro auto-detects `renderer.ts` in your project root and uses it for all non-API routes. The renderer function receives the request URL and returns a `Response`. Use `fetch` from `nitro` to call API routes without network overhead—these requests stay in-process. ## API Route ```ts [api/hello.ts] import { defineHandler } from "nitro"; export default defineHandler(() => "Nitro is amazing!"); ``` Define API routes in the `api/` directory. When the renderer calls `fetch("/api/hello")`, this handler runs and returns its response. ## Learn More - [Renderer](https://nitro.build/docs/renderer) # Runtime Config ::code-tree{expand-all default-value="nitro.config.ts"} ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ serverDir: "./", runtimeConfig: { apiKey: "", }, }); ``` ```json [package.json] { "type": "module", "scripts": { "dev": "nitro dev", "build": "nitro build" }, "devDependencies": { "nitro": "latest" } } ``` ```ts [server.ts] import { defineHandler } from "nitro"; import { useRuntimeConfig } from "nitro/runtime-config"; export default defineHandler((event) => { const runtimeConfig = useRuntimeConfig(); return { runtimeConfig }; }); ``` ```json [tsconfig.json] { "extends": "nitro/tsconfig" } ``` ```ts [vite.config.ts] import { defineConfig } from "vite"; import { nitro } from "nitro/vite"; export default defineConfig({ plugins: [nitro()] }); ``` :: Runtime config lets you define configuration values that can be overridden by environment variables at runtime. ## Define Config Schema Declare your runtime config with default values in `nitro.config.ts`: ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ serverDir: "./", runtimeConfig: { apiKey: "", }, }); ``` ## Access at Runtime Use `useRuntimeConfig` to access configuration values in your handlers: ```ts [server.ts] import { defineHandler } from "nitro"; import { useRuntimeConfig } from "nitro/runtime-config"; export default defineHandler((event) => { const runtimeConfig = useRuntimeConfig(); return { runtimeConfig }; }); ``` ## Environment Variables Override config values via environment variables prefixed with `NITRO_`: ```sh [.env] # NEVER COMMIT SENSITIVE DATA. THIS IS ONLY FOR DEMO PURPOSES. NITRO_API_KEY=secret-api-key ``` ## Learn More - [Configuration](https://nitro.build/docs/configuration) # Server Fetch ::code-tree{expand-all default-value="routes/index.ts"} ```ts [nitro.config.ts] import { defineConfig, serverFetch } from "nitro"; export default defineConfig({ serverDir: "./", hooks: { "dev:start": async () => { const res = await serverFetch("/hello"); const text = await res.text(); console.log("Fetched /hello in nitro module:", res.status, text); }, }, }); ``` ```json [package.json] { "type": "module", "scripts": { "dev": "nitro dev", "build": "nitro build" }, "devDependencies": { "nitro": "latest" } } ``` ```json [tsconfig.json] { "extends": "nitro/tsconfig" } ``` ```ts [vite.config.ts] import { defineConfig } from "vite"; import { nitro } from "nitro/vite"; export default defineConfig({ plugins: [nitro()] }); ``` ```ts [routes/hello.ts] import { defineHandler } from "nitro"; export default defineHandler(() => "Hello!"); ``` ```ts [routes/index.ts] import { defineHandler } from "nitro"; import { fetch } from "nitro"; export default defineHandler(() => fetch("/hello")); ``` :: When you need one route to call another, use Nitro's `fetch` function instead of the global fetch. It makes internal requests that stay in-process, avoiding network round-trips. The request never leaves the server. ## Main Route ```ts [routes/index.ts] import { defineHandler } from "nitro"; import { fetch } from "nitro"; export default defineHandler(() => fetch("/hello")); ``` The index route imports `fetch` from `nitro` (not the global fetch) and calls the `/hello` route. This request is handled internally without going through the network stack. ## Internal API Route ```ts [routes/hello.ts] import { defineHandler } from "nitro"; export default defineHandler(() => "Hello!"); ``` A simple route that returns "Hello!". When the index route calls `fetch("/hello")`, this handler runs and its response is returned directly. ## Learn More - [Routing](https://nitro.build/docs/routing) # Shiki ::code-tree{expand-all default-value="api/highlight.ts"} ```html [index.html] Hello World Snippet
JavaScript
{{{ hl(`console.log("💚 Simple is beautiful!");`) }}}
``` ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ serverDir: "./", }); ``` ```json [package.json] { "type": "module", "scripts": { "dev": "vite dev", "build": "vite build" }, "devDependencies": { "nitro": "latest", "shiki": "^3.22.0" } } ``` ```css [styles.css] html, body { height: 100%; margin: 0; } body { display: flex; align-items: center; justify-content: center; background: #f6f8fa; font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif; } .card { text-align: left; background: #0b1220; color: #e6edf3; padding: 1rem; border-radius: 8px; box-shadow: 0 8px 24px rgba(2, 6, 23, 0.2); max-width: 90%; width: 520px; } .label { font-size: 12px; color: #9aa7b2; margin-bottom: 8px; } pre { margin: 0; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, "Courier New", monospace; font-size: 14px; background: transparent; white-space: pre; overflow: auto; } ``` ```json [tsconfig.json] { "extends": "nitro/tsconfig" } ``` ```ts [vite.config.ts] import { defineConfig } from "vite"; import { nitro } from "nitro/vite"; export default defineConfig({ plugins: [nitro()], }); ``` ```ts [api/highlight.ts] import { createHighlighterCore } from "shiki/core"; import { createOnigurumaEngine } from "shiki/engine/oniguruma"; const highlighter = await createHighlighterCore({ engine: createOnigurumaEngine(import("shiki/wasm")), themes: [await import("shiki/themes/vitesse-dark.mjs")], langs: [await import("shiki/langs/ts.mjs")], }); export default async ({ req }: { req: Request }) => { const code = await req.text(); const html = await highlighter.codeToHtml(code, { lang: "ts", theme: "vitesse-dark", }); return new Response(html, { headers: { "Content-Type": "text/html; charset=utf-8" }, }); }; ``` :: Use Shiki for syntax highlighting with TextMate grammars. This example highlights code on the server using Nitro's server scripts feature, which runs JavaScript inside HTML files before sending the response. ## API Route ```ts [api/highlight.ts] import { createHighlighterCore } from "shiki/core"; import { createOnigurumaEngine } from "shiki/engine/oniguruma"; const highlighter = await createHighlighterCore({ engine: createOnigurumaEngine(import("shiki/wasm")), themes: [await import("shiki/themes/vitesse-dark.mjs")], langs: [await import("shiki/langs/ts.mjs")], }); export default async ({ req }: { req: Request }) => { const code = await req.text(); const html = await highlighter.codeToHtml(code, { lang: "ts", theme: "vitesse-dark", }); return new Response(html, { headers: { "Content-Type": "text/html; charset=utf-8" }, }); }; ``` Create a Shiki highlighter with the Vitesse Dark theme and TypeScript language support. When the API receives a POST request, it reads the code from the request body and returns highlighted HTML. ## Server-Side Rendering ```html [index.html] Hello World Snippet
JavaScript
{{{ hl(`console.log("💚 Simple is beautiful!");`) }}}
``` The ` rscStream ); // Browser root component to (re-)render RSC payload as state function BrowserRoot() { const [payload, setPayload_] = React.useState(initialPayload); React.useEffect(() => { setPayload = (v) => React.startTransition(() => setPayload_(v)); }, [setPayload_]); // Re-fetch/render on client side navigation React.useEffect(() => { return listenNavigation(() => fetchRscPayload()); }, []); return payload.root; } // Re-fetch RSC and trigger re-rendering async function fetchRscPayload() { const renderRequest = createRscRenderRequest(globalThis.location.href); const payload = await createFromFetch(fetch(renderRequest)); setPayload(payload); } // Register a handler which will be internally called by React // on server function request after hydration. setServerCallback(async (id, args) => { const temporaryReferences = createTemporaryReferenceSet(); const renderRequest = createRscRenderRequest(globalThis.location.href, { id, body: await encodeReply(args, { temporaryReferences }), }); const payload = await createFromFetch(fetch(renderRequest), { temporaryReferences, }); setPayload(payload); const { ok, data } = payload.returnValue!; if (!ok) throw data; return data; }); // Hydration const browserRoot = ( ); if ("__NO_HYDRATE" in globalThis) { createRoot(document).render(browserRoot); } else { hydrateRoot(document, browserRoot, { formState: initialPayload.formState, }); } // Implement server HMR by triggering re-fetch/render of RSC upon server code change if (import.meta.hot) { import.meta.hot.on("rsc:update", () => { fetchRscPayload(); }); } } // A little helper to setup events interception for client side navigation function listenNavigation(onNavigation: () => void) { globalThis.addEventListener("popstate", onNavigation); const oldPushState = globalThis.history.pushState; globalThis.history.pushState = function (...args) { const res = oldPushState.apply(this, args); onNavigation(); return res; }; const oldReplaceState = globalThis.history.replaceState; globalThis.history.replaceState = function (...args) { const res = oldReplaceState.apply(this, args); onNavigation(); return res; }; function onClick(e: MouseEvent) { const link = (e.target as Element).closest("a"); if ( link && link instanceof HTMLAnchorElement && link.href && (!link.target || link.target === "_self") && link.origin === location.origin && !link.hasAttribute("download") && e.button === 0 && // left clicks only !e.metaKey && // open in new tab (mac) !e.ctrlKey && // open in new tab (windows) !e.altKey && // download !e.shiftKey && !e.defaultPrevented ) { e.preventDefault(); history.pushState(null, "", link.href); } } document.addEventListener("click", onClick); return () => { document.removeEventListener("click", onClick); globalThis.removeEventListener("popstate", onNavigation); globalThis.history.pushState = oldPushState; globalThis.history.replaceState = oldReplaceState; }; } // eslint-disable-next-line unicorn/prefer-top-level-await main(); ``` ```tsx [app/framework/entry.rsc.tsx] import { renderToReadableStream, createTemporaryReferenceSet, decodeReply, loadServerAction, decodeAction, decodeFormState, } from "@vitejs/plugin-rsc/rsc"; import type { ReactFormState } from "react-dom/client"; import { Root } from "../root.tsx"; import { parseRenderRequest } from "./request.tsx"; // The schema of payload which is serialized into RSC stream on rsc environment // and deserialized on ssr/client environments. export type RscPayload = { // this demo renders/serializes/deserializes entire root html element // but this mechanism can be changed to render/fetch different parts of components // based on your own route conventions. root: React.ReactNode; // Server action return value of non-progressive enhancement case returnValue?: { ok: boolean; data: unknown }; // Server action form state (e.g. useActionState) of progressive enhancement case formState?: ReactFormState; }; // The plugin by default assumes `rsc` entry having default export of request handler. // however, how server entries are executed can be customized by registering own server handler. export default async function handler(request: Request): Promise { // Differentiate RSC, SSR, action, etc. const renderRequest = parseRenderRequest(request); request = renderRequest.request; // Handle server function request let returnValue: RscPayload["returnValue"] | undefined; let formState: ReactFormState | undefined; let temporaryReferences: unknown | undefined; let actionStatus: number | undefined; if (renderRequest.isAction === true) { if (renderRequest.actionId) { // Action is called via `ReactClient.setServerCallback`. const contentType = request.headers.get("content-type"); const body = contentType?.startsWith("multipart/form-data") ? await request.formData() : await request.text(); temporaryReferences = createTemporaryReferenceSet(); const args = await decodeReply(body, { temporaryReferences }); const action = await loadServerAction(renderRequest.actionId); try { // eslint-disable-next-line prefer-spread const data = await action.apply(null, args); returnValue = { ok: true, data }; } catch (error_) { returnValue = { ok: false, data: error_ }; actionStatus = 500; } } else { // Otherwise server function is called via `
` // before hydration (e.g. when JavaScript is disabled). // aka progressive enhancement. const formData = await request.formData(); const decodedAction = await decodeAction(formData); try { const result = await decodedAction(); formState = await decodeFormState(result, formData); } catch { // there's no single general obvious way to surface this error, // so explicitly return classic 500 response. return new Response("Internal Server Error: server action failed", { status: 500, }); } } } // Serialization from React VDOM tree to RSC stream. // We render RSC stream after handling server function request // so that new render reflects updated state from server function call // to achieve single round trip to mutate and fetch from server. const rscPayload: RscPayload = { root: , formState, returnValue, }; const rscOptions = { temporaryReferences }; const rscStream = renderToReadableStream(rscPayload, rscOptions); // Respond RSC stream without HTML rendering as decided by `RenderRequest` if (renderRequest.isRsc) { return new Response(rscStream, { status: actionStatus, headers: { "content-type": "text/x-component;charset=utf-8", }, }); } // Delegate to SSR environment for HTML rendering. // The plugin provides `loadModule` helper to allow loading SSR environment entry module // in RSC environment. however this can be customized by implementing own runtime communication // e.g. `@cloudflare/vite-plugin`'s service binding. const ssrEntryModule = await import.meta.viteRsc.loadModule( "ssr", "index" ); const ssrResult = await ssrEntryModule.renderHTML(rscStream, { formState, // Allow quick simulation of JavaScript disabled browser debugNoJS: renderRequest.url.searchParams.has("__nojs"), }); // Respond HTML return new Response(ssrResult.stream, { status: ssrResult.status, headers: { "Content-Type": "text/html", }, }); } if (import.meta.hot) { import.meta.hot.accept(); } ``` ```tsx [app/framework/entry.ssr.tsx] import { createFromReadableStream } from "@vitejs/plugin-rsc/ssr"; import React from "react"; import type { ReactFormState } from "react-dom/client"; import { renderToReadableStream } from "react-dom/server.edge"; import { injectRSCPayload } from "rsc-html-stream/server"; import type { RscPayload } from "./entry.rsc"; export default { fetch: async (request: Request) => { const rscEntryModule = await import.meta.viteRsc.loadModule( "rsc", "index" ); return rscEntryModule.default(request); }, }; export async function renderHTML( rscStream: ReadableStream, options: { formState?: ReactFormState; nonce?: string; debugNoJS?: boolean; } ): Promise<{ stream: ReadableStream; status?: number }> { // Duplicate one RSC stream into two. // - one for SSR (ReactClient.createFromReadableStream below) // - another for browser hydration payload by injecting . const [rscStream1, rscStream2] = rscStream.tee(); // Deserialize RSC stream back to React VDOM let payload: Promise | undefined; function SsrRoot() { // Deserialization needs to be kicked off inside ReactDOMServer context // for ReactDOMServer preinit/preloading to work payload ??= createFromReadableStream(rscStream1); return React.use(payload).root; } // Render HTML (traditional SSR) const bootstrapScriptContent = await import.meta.viteRsc.loadBootstrapScriptContent("index"); let htmlStream: ReadableStream; let status: number | undefined; try { htmlStream = await renderToReadableStream(, { bootstrapScriptContent: options?.debugNoJS ? undefined : bootstrapScriptContent, nonce: options?.nonce, formState: options?.formState, }); } catch { // fallback to render an empty shell and run pure CSR on browser, // which can replay server component error and trigger error boundary. status = 500; htmlStream = await renderToReadableStream( , { bootstrapScriptContent: `self.__NO_HYDRATE=1;` + (options?.debugNoJS ? "" : bootstrapScriptContent), nonce: options?.nonce, } ); } let responseStream: ReadableStream = htmlStream; if (!options?.debugNoJS) { // Initial RSC stream is injected in HTML stream as // using utility made by devongovett https://github.com/devongovett/rsc-html-stream responseStream = responseStream.pipeThrough( injectRSCPayload(rscStream2, { nonce: options?.nonce, }) ); } return { stream: responseStream, status }; } ``` ```tsx [app/framework/error-boundary.tsx] "use client"; import React from "react"; // Minimal ErrorBoundary example to handle errors globally on browser export function GlobalErrorBoundary(props: { children?: React.ReactNode }) { return {props.children}; } // https://github.com/vercel/next.js/blob/33f8428f7066bf8b2ec61f025427ceb2a54c4bdf/packages/next/src/client/components/error-boundary.tsx // https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary class ErrorBoundary extends React.Component<{ children?: React.ReactNode; errorComponent: React.FC<{ error: Error; reset: () => void; }>; }> { override state: { error?: Error } = {}; static getDerivedStateFromError(error: Error) { return { error }; } reset = () => { this.setState({ error: null }); }; override render() { const error = this.state.error; if (error) { return ; } return this.props.children; } } // https://github.com/vercel/next.js/blob/677c9b372faef680d17e9ba224743f44e1107661/packages/next/src/build/webpack/loaders/next-app-loader.ts#L73 // https://github.com/vercel/next.js/blob/677c9b372faef680d17e9ba224743f44e1107661/packages/next/src/client/components/error-boundary.tsx#L145 function DefaultGlobalErrorPage(props: { error: Error; reset: () => void }) { return ( Unexpected Error

Caught an unexpected error

          Error:{" "}
          {import.meta.env.DEV && "message" in props.error ? props.error.message : "(Unknown)"}
        
); } ``` ```tsx [app/framework/request.tsx] // Framework conventions (arbitrary choices for this demo): // - Use `_.rsc` URL suffix to differentiate RSC requests from SSR requests // - Use `x-rsc-action` header to pass server action ID const URL_POSTFIX = "_.rsc"; const HEADER_ACTION_ID = "x-rsc-action"; // Parsed request information used to route between RSC/SSR rendering and action handling. // Created by parseRenderRequest() from incoming HTTP requests. type RenderRequest = { isRsc: boolean; // true if request should return RSC payload (via _.rsc suffix) isAction: boolean; // true if this is a server action call (POST request) actionId?: string; // server action ID from x-rsc-action header request: Request; // normalized Request with _.rsc suffix removed from URL url: URL; // normalized URL with _.rsc suffix removed }; export function createRscRenderRequest( urlString: string, action?: { id: string; body: BodyInit } ): Request { const url = new URL(urlString); url.pathname += URL_POSTFIX; const headers = new Headers(); if (action) { headers.set(HEADER_ACTION_ID, action.id); } return new Request(url.toString(), { method: action ? "POST" : "GET", headers, body: action?.body, }); } export function parseRenderRequest(request: Request): RenderRequest { const url = new URL(request.url); const isAction = request.method === "POST"; if (url.pathname.endsWith(URL_POSTFIX)) { url.pathname = url.pathname.slice(0, -URL_POSTFIX.length); const actionId = request.headers.get(HEADER_ACTION_ID) || undefined; if (request.method === "POST" && !actionId) { throw new Error("Missing action id header for RSC action request"); } return { isRsc: true, isAction, actionId, request: new Request(url, request), url, }; } else { return { isRsc: false, isAction, request, url, }; } } ``` :: This example demonstrates React Server Components (RSC) using Vite's experimental RSC plugin with Nitro. It includes server components, client components, server actions, and streaming SSR. ## Overview ::steps{level="4"} #### **SSR Entry** handles incoming requests and renders React components to HTML #### **Root Component** defines the page structure as a server component #### **Client Components** use the `"use client"` directive for interactive parts :: ## 1. SSR Entry ```tsx [app/framework/entry.ssr.tsx] import { createFromReadableStream } from "@vitejs/plugin-rsc/ssr"; import React from "react"; import type { ReactFormState } from "react-dom/client"; import { renderToReadableStream } from "react-dom/server.edge"; import { injectRSCPayload } from "rsc-html-stream/server"; import type { RscPayload } from "./entry.rsc"; export default { fetch: async (request: Request) => { const rscEntryModule = await import.meta.viteRsc.loadModule( "rsc", "index" ); return rscEntryModule.default(request); }, }; export async function renderHTML( rscStream: ReadableStream, options: { formState?: ReactFormState; nonce?: string; debugNoJS?: boolean; } ): Promise<{ stream: ReadableStream; status?: number }> { // Duplicate one RSC stream into two. // - one for SSR (ReactClient.createFromReadableStream below) // - another for browser hydration payload by injecting . const [rscStream1, rscStream2] = rscStream.tee(); // Deserialize RSC stream back to React VDOM let payload: Promise | undefined; function SsrRoot() { // Deserialization needs to be kicked off inside ReactDOMServer context // for ReactDOMServer preinit/preloading to work payload ??= createFromReadableStream(rscStream1); return React.use(payload).root; } // Render HTML (traditional SSR) const bootstrapScriptContent = await import.meta.viteRsc.loadBootstrapScriptContent("index"); let htmlStream: ReadableStream; let status: number | undefined; try { htmlStream = await renderToReadableStream(, { bootstrapScriptContent: options?.debugNoJS ? undefined : bootstrapScriptContent, nonce: options?.nonce, formState: options?.formState, }); } catch { // fallback to render an empty shell and run pure CSR on browser, // which can replay server component error and trigger error boundary. status = 500; htmlStream = await renderToReadableStream( , { bootstrapScriptContent: `self.__NO_HYDRATE=1;` + (options?.debugNoJS ? "" : bootstrapScriptContent), nonce: options?.nonce, } ); } let responseStream: ReadableStream = htmlStream; if (!options?.debugNoJS) { // Initial RSC stream is injected in HTML stream as // using utility made by devongovett https://github.com/devongovett/rsc-html-stream responseStream = responseStream.pipeThrough( injectRSCPayload(rscStream2, { nonce: options?.nonce, }) ); } return { stream: responseStream, status }; } ``` The SSR entry handles the rendering pipeline. It loads the RSC entry module, duplicates the RSC stream (one for SSR, one for hydration), deserializes the stream back to React VDOM, and renders it to HTML. The RSC payload is injected into the HTML for client hydration. ## 2. Root Server Component ```tsx [app/root.tsx] import "./index.css"; // css import is automatically injected in exported server components import viteLogo from "./assets/vite.svg"; import { getServerCounter, updateServerCounter } from "./action.tsx"; import reactLogo from "./assets/react.svg"; import nitroLogo from "./assets/nitro.svg"; import { ClientCounter } from "./client.tsx"; export function Root(props: { url: URL }) { return ( {/* eslint-disable-next-line unicorn/text-encoding-identifier-case */} Nitro + Vite + RSC ); } function App(props: { url: URL }) { return (

Vite + RSC + Nitro

Request URL: {props.url?.href}
  • Edit src/client.tsx to test client HMR.
  • Edit src/root.tsx to test server HMR.
  • Visit{" "} _.rsc {" "} to view RSC stream payload.
  • Visit{" "} ?__nojs {" "} to test server action without js enabled.
); } ``` Server components run only on the server. They can import CSS directly, use server-side data, and call server actions. The `ClientCounter` component is imported but runs on the client because it has the `"use client"` directive. ## 3. Client Component ```tsx [app/client.tsx] "use client"; import React from "react"; export function ClientCounter() { const [count, setCount] = React.useState(0); return ; } ``` The `"use client"` directive marks this as a client component. It hydrates on the browser and handles interactive state. Server components can import and render client components, but client components cannot import server components. ## Learn More - [React Server Components](https://react.dev/reference/rsc/server-components){rel=""nofollow""} # Vite SSR HTML ::code-tree{expand-all default-value="app/entry-server.ts"} ```html [index.html] Nitro Quotes
Powered by Vite and Nitro v3.
``` ```json [package.json] { "type": "module", "scripts": { "build": "vite build", "dev": "vite dev", "preview": "vite preview" }, "devDependencies": { "@tailwindcss/vite": "^4.1.18", "nitro": "latest", "tailwindcss": "^4.1.18", "vite": "beta" } } ``` ```json [tsconfig.json] { "extends": "nitro/tsconfig" } ``` ```ts [vite.config.ts] import { defineConfig } from "vite"; import { nitro } from "nitro/vite"; import tailwindcss from "@tailwindcss/vite"; export default defineConfig({ plugins: [ nitro({ serverDir: "./", }), tailwindcss(), ], }); ``` ```ts [app/entry-server.ts] import { fetch } from "nitro"; export default { async fetch() { const quote = (await fetch("/quote").then((res) => res.json())) as { text: string; }; return tokenizedStream(quote.text, 50); }, }; function tokenizedStream(text: string, delay: number): ReadableStream { const tokens = text.split(" "); return new ReadableStream({ start(controller) { let index = 0; function push() { if (index < tokens.length) { const word = tokens[index++] + (index < tokens.length ? " " : ""); controller.enqueue(new TextEncoder().encode(word)); setTimeout(push, delay); } else { controller.close(); } } push(); }, }); } ``` ```ts [routes/quote.ts] const QUOTES_URL = "https://github.com/JamesFT/Database-Quotes-JSON/raw/refs/heads/master/quotes.json"; let _quotes: Promise | undefined; function getQuotes() { return (_quotes ??= fetch(QUOTES_URL).then((res) => res.json())) as Promise< { quoteText: string; quoteAuthor: string }[] >; } export default async function quotesHandler() { const quotes = await getQuotes(); const randomQuote = quotes[Math.floor(Math.random() * quotes.length)]; return Response.json({ text: randomQuote.quoteText, author: randomQuote.quoteAuthor, }); } ``` :: This example renders an HTML template with server-side data and streams the response word by word. It demonstrates how to use Nitro's Vite SSR integration without a framework. ## Overview ::steps{level="4"} #### **Add the Nitro Vite plugin** to enable SSR #### **Create an HTML template** with a `` comment where server content goes #### **Create a server entry** that fetches data and returns a stream #### **Add API routes** for server-side data :: ## How It Works The `index.html` file contains an `` comment that marks where server-rendered content will be inserted. Nitro replaces this comment with the output from your server entry. The server entry exports an object with a `fetch` method. It calls the `/quote` API route using Nitro's internal fetch, then returns a `ReadableStream` that emits the quote text word by word with a 50ms delay between each word. The quote route fetches a JSON file of quotes from GitHub, caches the result, and returns a random quote. The server entry calls this route to get content for the page. ## Learn More - [Renderer](https://nitro.build/docs/renderer) - [Server Entry](https://nitro.build/docs/server-entry) # SSR with Preact ::code-tree{expand-all default-value="src/entry-server.tsx"} ```json [package.json] { "type": "module", "scripts": { "build": "vite build", "preview": "vite preview", "dev": "vite dev" }, "devDependencies": { "@preact/preset-vite": "^2.10.3", "@tailwindcss/vite": "^4.1.18", "nitro": "latest", "preact": "^10.28.3", "preact-render-to-string": "^6.6.5", "tailwindcss": "^4.1.18", "vite": "beta" } } ``` ```json [tsconfig.json] { "extends": "nitro/tsconfig", "compilerOptions": { "jsx": "react-jsx", "jsxImportSource": "preact" } } ``` ```js [vite.config.mjs] import { defineConfig } from "vite"; import { nitro } from "nitro/vite"; import preact from "@preact/preset-vite"; export default defineConfig({ plugins: [nitro(), preact()], environments: { client: { build: { rollupOptions: { input: "./src/entry-client.tsx", }, }, }, }, }); ``` ```tsx [src/app.tsx] import { useState } from "preact/hooks"; export function App() { const [count, setCount] = useState(0); return ; } ``` ```tsx [src/entry-client.tsx] import { hydrate } from "preact"; import { App } from "./app.tsx"; function main() { hydrate(, document.querySelector("#app")!); } main(); ``` ```tsx [src/entry-server.tsx] import "./styles.css"; import { renderToReadableStream } from "preact-render-to-string/stream"; import { App } from "./app.jsx"; import clientAssets from "./entry-client?assets=client"; import serverAssets from "./entry-server?assets=ssr"; export default { async fetch(request: Request) { const url = new URL(request.url); const htmlStream = renderToReadableStream(); return new Response(htmlStream, { headers: { "Content-Type": "text/html;charset=utf-8" }, }); }, }; function Root(props: { url: URL }) { const assets = clientAssets.merge(serverAssets); return ( {assets.css.map((attr: any) => ( ))} {assets.js.map((attr: any) => ( ))} ``` ```json [package.json] { "type": "module", "scripts": { "build": "vite build", "dev": "vite dev", "preview": "vite preview" }, "devDependencies": { "@tanstack/react-router": "^1.158.1", "@tanstack/react-router-devtools": "^1.158.1", "@tanstack/router-plugin": "^1.158.1", "@types/react": "^19.2.13", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^5.1.3", "nitro": "latest", "react": "^19.2.4", "react-dom": "^19.2.4", "vite": "beta" } } ``` ```json [tsconfig.json] { "extends": "nitro/tsconfig", "compilerOptions": { "baseUrl": ".", "jsx": "react-jsx", "paths": { "@/*": ["sec/*"] } } } ``` ```js [vite.config.mjs] import { defineConfig } from "vite"; import { nitro } from "nitro/vite"; import react from "@vitejs/plugin-react"; import { tanstackRouter } from "@tanstack/router-plugin/vite"; export default defineConfig({ plugins: [tanstackRouter({ target: "react", autoCodeSplitting: true }), react(), nitro()], }); ``` ```tsx [src/main.tsx] import { StrictMode } from "react"; import ReactDOM from "react-dom/client"; import { RouterProvider, createRouter } from "@tanstack/react-router"; // Import the generated route tree import { routeTree } from "./routeTree.gen.ts"; // Create a new router instance const router = createRouter({ routeTree }); // Register the router instance for type safety declare module "@tanstack/react-router" { interface Register { router: typeof router; } } // Render the app const rootElement = document.querySelector("#root")!; if (!rootElement.innerHTML) { const root = ReactDOM.createRoot(rootElement); root.render( ); } ``` ```ts [src/routeTree.gen.ts] /* eslint-disable */ // @ts-nocheck // noinspection JSUnusedGlobalSymbols // This file was automatically generated by TanStack Router. // You should NOT make any changes in this file as it will be overwritten. // Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. import { Route as rootRouteImport } from './routes/__root' import { Route as IndexRouteImport } from './routes/index' const IndexRoute = IndexRouteImport.update({ id: '/', path: '/', getParentRoute: () => rootRouteImport, } as any) export interface FileRoutesByFullPath { '/': typeof IndexRoute } export interface FileRoutesByTo { '/': typeof IndexRoute } export interface FileRoutesById { __root__: typeof rootRouteImport '/': typeof IndexRoute } export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath fullPaths: '/' fileRoutesByTo: FileRoutesByTo to: '/' id: '__root__' | '/' fileRoutesById: FileRoutesById } export interface RootRouteChildren { IndexRoute: typeof IndexRoute } declare module '@tanstack/react-router' { interface FileRoutesByPath { '/': { id: '/' path: '/' fullPath: '/' preLoaderRoute: typeof IndexRouteImport parentRoute: typeof rootRouteImport } } } const rootRouteChildren: RootRouteChildren = { IndexRoute: IndexRoute, } export const routeTree = rootRouteImport ._addFileChildren(rootRouteChildren) ._addFileTypes() ``` ```css [src/assets/main.css] :root { font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; line-height: 1.5; font-weight: 400; color-scheme: light dark; color: rgba(255, 255, 255, 0.87); background-color: #242424; font-synthesis: none; text-rendering: optimizeLegibility; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } a { font-weight: 500; color: #ff2056; text-decoration: inherit; } a:hover { color: #ff637e; } body { margin: 0; display: flex; flex-direction: column; place-items: center; justify-content: center; min-width: 320px; min-height: 100vh; } h1 { font-size: 3.2em; line-height: 1.1; } #app { max-width: 1280px; margin: 0 auto; padding: 2rem; text-align: center; } .logo { height: 6em; padding: 1.5em; will-change: filter; transition: filter 300ms; transition: transform 300ms; } .logo:hover { transform: scale(1.1); } .card { padding: 2em; } .read-the-docs { color: #888; } button { border-radius: 8px; border: 1px solid transparent; padding: 0.6em 1.2em; font-size: 1em; font-weight: 500; font-family: inherit; background-color: #1a1a1a; cursor: pointer; transition: border-color 0.25s; } button:hover { border-color: #646cff; } button:focus, button:focus-visible { outline: 4px auto -webkit-focus-ring-color; } @media (prefers-color-scheme: light) { :root { color: #213547; background-color: #ffffff; } a:hover { color: #747bff; } button { background-color: #f9f9f9; } } ``` ```tsx [src/routes/__root.tsx] import { createRootRoute, Link, Outlet } from "@tanstack/react-router"; import { TanStackRouterDevtools } from "@tanstack/react-router-devtools"; const RootLayout = () => ( <>
Home

); export const Route = createRootRoute({ component: RootLayout }); ``` ```tsx [src/routes/index.tsx] import { createFileRoute } from "@tanstack/react-router"; export const Route = createFileRoute("/")({ loader: async () => { const r = await fetch("/api/hello"); return r.json(); }, component: Index, }); function Index() { const r = Route.useLoaderData(); return (

{JSON.stringify(r)}

); } ``` :: Set up TanStack Router with React, Vite, and Nitro. This setup provides file-based routing with type-safe navigation and automatic code splitting. ## Overview ::steps{level="4"} #### Add the Nitro Vite plugin to your Vite config #### Create an HTML template with your app entry #### Create a main entry that initializes the router #### Define routes using file-based routing :: ## 1. Configure Vite Add the Nitro, React, and TanStack Router plugins to your Vite config: ```js [vite.config.mjs] import { defineConfig } from "vite"; import { nitro } from "nitro/vite"; import react from "@vitejs/plugin-react"; import { tanstackRouter } from "@tanstack/router-plugin/vite"; export default defineConfig({ plugins: [tanstackRouter({ target: "react", autoCodeSplitting: true }), react(), nitro()], }); ``` The `tanstackRouter` plugin generates a route tree from your `routes/` directory structure. Enable `autoCodeSplitting` to automatically split routes into separate chunks. Place the TanStack Router plugin before the React plugin in the array. ## 2. Create the HTML Template Create an HTML file that serves as your app shell: ```html [index.html] Nitro + TanStack Router + React
``` ## 3. Create the App Entry Create the main entry that initializes TanStack Router: ```tsx [src/main.tsx] import { StrictMode } from "react"; import ReactDOM from "react-dom/client"; import { RouterProvider, createRouter } from "@tanstack/react-router"; // Import the generated route tree import { routeTree } from "./routeTree.gen.ts"; // Create a new router instance const router = createRouter({ routeTree }); // Register the router instance for type safety declare module "@tanstack/react-router" { interface Register { router: typeof router; } } // Render the app const rootElement = document.querySelector("#root")!; if (!rootElement.innerHTML) { const root = ReactDOM.createRoot(rootElement); root.render( ); } ``` The `routeTree.gen.ts` file is auto-generated from your `routes/` directory structure. The `Register` interface declaration provides full type inference for route paths and params. The `!rootElement.innerHTML` check prevents re-rendering during hot module replacement. ## 4. Create the Root Route The root route (`__root.tsx`) defines your app's layout: ```tsx [src/routes/__root.tsx] import { createRootRoute, Link, Outlet } from "@tanstack/react-router"; import { TanStackRouterDevtools } from "@tanstack/react-router-devtools"; const RootLayout = () => ( <>
Home

); export const Route = createRootRoute({ component: RootLayout }); ``` Use `Link` for type-safe navigation with active state styling. The `Outlet` component renders child routes. Include `TanStackRouterDevtools` for development tools (automatically removed in production). ## 5. Create Page Routes Page routes use `createFileRoute` and can include loaders: ```tsx [src/routes/index.tsx] import { createFileRoute } from "@tanstack/react-router"; export const Route = createFileRoute("/")({ loader: async () => { const r = await fetch("/api/hello"); return r.json(); }, component: Index, }); function Index() { const r = Route.useLoaderData(); return (

{JSON.stringify(r)}

); } ``` Fetch data before rendering with the `loader` function—data is available via `Route.useLoaderData()`. File paths determine URL paths: `routes/index.tsx` maps to `/`, `routes/about.tsx` to `/about`, and `routes/users/$id.tsx` to `/users/:id`. ## Learn More - [TanStack Router Documentation](https://tanstack.com/router){rel=""nofollow""} - [Renderer](https://nitro.build/docs/renderer) # SSR with TanStack Start ::code-tree{expand-all default-value="server.ts"} ```json [package.json] { "type": "module", "scripts": { "build": "vite build", "dev": "vite dev", "start": "node .output/server/index.mjs" }, "dependencies": { "@tanstack/react-router": "^1.158.1", "@tanstack/react-router-devtools": "^1.158.1", "@tanstack/react-start": "^1.158.3", "nitro": "latest", "react": "^19.2.4", "react-dom": "^19.2.4", "tailwind-merge": "^3.4.0", "zod": "^4.3.6" }, "devDependencies": { "@tailwindcss/vite": "^4.1.18", "@types/node": "latest", "@types/react": "^19.2.13", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^5.1.3", "tailwindcss": "^4.1.18", "typescript": "^5.9.3", "vite": "beta", "vite-tsconfig-paths": "^6.0.5" } } ``` ```ts [server.ts] import handler, { createServerEntry } from "@tanstack/react-start/server-entry"; export default createServerEntry({ fetch(request) { return handler.fetch(request); }, }); ``` ```json [tsconfig.json] { "extends": "nitro/tsconfig", "compilerOptions": { "baseUrl": ".", "jsx": "react-jsx", "paths": { "~/*": ["./src/*"] } } } ``` ```js [vite.config.mjs] import { defineConfig } from "vite"; import { nitro } from "nitro/vite"; import { tanstackStart } from "@tanstack/react-start/plugin/vite"; import viteReact from "@vitejs/plugin-react"; import viteTsConfigPaths from "vite-tsconfig-paths"; import tailwindcss from "@tailwindcss/vite"; export default defineConfig({ plugins: [ viteTsConfigPaths({ projects: ["./tsconfig.json"] }), tanstackStart(), viteReact(), tailwindcss(), nitro(), ], environments: { ssr: { build: { rollupOptions: { input: "./server.ts" } } }, }, }); ``` ```tsx [src/router.tsx] import { createRouter } from "@tanstack/react-router"; import { routeTree } from "./routeTree.gen.ts"; export function getRouter() { const router = createRouter({ routeTree, defaultPreload: "intent", defaultErrorComponent: () =>
Internal Server Error
, defaultNotFoundComponent: () =>
Not Found
, scrollRestoration: true, }); return router; } ``` ```ts [src/routeTree.gen.ts] /* eslint-disable */ // @ts-nocheck // noinspection JSUnusedGlobalSymbols // This file was automatically generated by TanStack Router. // You should NOT make any changes in this file as it will be overwritten. // Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. import { Route as rootRouteImport } from './routes/__root' import { Route as IndexRouteImport } from './routes/index' import { Route as ApiTestRouteImport } from './routes/api/test' const IndexRoute = IndexRouteImport.update({ id: '/', path: '/', getParentRoute: () => rootRouteImport, } as any) const ApiTestRoute = ApiTestRouteImport.update({ id: '/api/test', path: '/api/test', getParentRoute: () => rootRouteImport, } as any) export interface FileRoutesByFullPath { '/': typeof IndexRoute '/api/test': typeof ApiTestRoute } export interface FileRoutesByTo { '/': typeof IndexRoute '/api/test': typeof ApiTestRoute } export interface FileRoutesById { __root__: typeof rootRouteImport '/': typeof IndexRoute '/api/test': typeof ApiTestRoute } export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath fullPaths: '/' | '/api/test' fileRoutesByTo: FileRoutesByTo to: '/' | '/api/test' id: '__root__' | '/' | '/api/test' fileRoutesById: FileRoutesById } export interface RootRouteChildren { IndexRoute: typeof IndexRoute ApiTestRoute: typeof ApiTestRoute } declare module '@tanstack/react-router' { interface FileRoutesByPath { '/': { id: '/' path: '/' fullPath: '/' preLoaderRoute: typeof IndexRouteImport parentRoute: typeof rootRouteImport } '/api/test': { id: '/api/test' path: '/api/test' fullPath: '/api/test' preLoaderRoute: typeof ApiTestRouteImport parentRoute: typeof rootRouteImport } } } const rootRouteChildren: RootRouteChildren = { IndexRoute: IndexRoute, ApiTestRoute: ApiTestRoute, } export const routeTree = rootRouteImport ._addFileChildren(rootRouteChildren) ._addFileTypes() import type { getRouter } from './router.tsx' import type { createStart } from '@tanstack/react-start' declare module '@tanstack/react-start' { interface Register { ssr: true router: Awaited> } } ``` ```tsx [src/routes/__root.tsx] /// import { HeadContent, Link, Scripts, createRootRoute } from "@tanstack/react-router"; import { TanStackRouterDevtools } from "@tanstack/react-router-devtools"; import * as React from "react"; import appCss from "~/styles/app.css?url"; export const Route = createRootRoute({ head: () => ({ meta: [ { charSet: "utf8" }, { name: "viewport", content: "width=device-width, initial-scale=1" }, ], links: [{ rel: "stylesheet", href: appCss }], scripts: [{ src: "/customScript.js", type: "text/javascript" }], }), errorComponent: () =>

500: Internal Server Error

, notFoundComponent: () =>

404: Page Not Found

, shellComponent: RootDocument, }); function RootDocument({ children }: { children: React.ReactNode }) { return (
Home {" "} 404

{children} ); } ``` ```tsx [src/routes/index.tsx] import { createFileRoute } from "@tanstack/react-router"; export const Route = createFileRoute("/")({ component: Home }); function Home() { return (

Welcome Home!

/api/test
); } ``` ```css [src/styles/app.css] @import "tailwindcss"; @layer base { *, ::after, ::before, ::backdrop, ::file-selector-button { border-color: var(--color-gray-200, currentcolor); } } @layer base { html { color-scheme: light dark; } * { @apply border-gray-200 dark:border-gray-800; } html, body { @apply text-gray-900 bg-gray-50 dark:bg-gray-950 dark:text-gray-200; } .using-mouse * { outline: none !important; } } ``` :: Set up TanStack Start with Nitro for a full-stack React framework experience with server-side rendering, file-based routing, and integrated API routes. ## Overview ::steps{level="4"} #### Add the Nitro Vite plugin to your Vite config #### Create a server entry using TanStack Start's server handler #### Configure the router with default components #### Define routes and API endpoints using file-based routing :: ## 1. Configure Vite Add the Nitro, React, TanStack Start, and Tailwind plugins to your Vite config: ```js [vite.config.mjs] import { defineConfig } from "vite"; import { nitro } from "nitro/vite"; import { tanstackStart } from "@tanstack/react-start/plugin/vite"; import viteReact from "@vitejs/plugin-react"; import viteTsConfigPaths from "vite-tsconfig-paths"; import tailwindcss from "@tailwindcss/vite"; export default defineConfig({ plugins: [ viteTsConfigPaths({ projects: ["./tsconfig.json"] }), tanstackStart(), viteReact(), tailwindcss(), nitro(), ], environments: { ssr: { build: { rollupOptions: { input: "./server.ts" } } }, }, }); ``` The `tanstackStart()` plugin provides full SSR integration with automatic client entry handling. Use `viteTsConfigPaths()` to enable path aliases like `~/` from tsconfig. The `environments.ssr` option points to the server entry file. ## 2. Create the Server Entry Create a server entry that uses TanStack Start's handler: ```ts [server.ts] import handler, { createServerEntry } from "@tanstack/react-start/server-entry"; export default createServerEntry({ fetch(request) { return handler.fetch(request); }, }); ``` TanStack Start handles SSR automatically. The `createServerEntry` wrapper integrates with Nitro's server entry format, and the `handler.fetch` processes all incoming requests. ## 3. Configure the Router Create a router factory function with default error and not-found components: ```tsx [src/router.tsx] import { createRouter } from "@tanstack/react-router"; import { routeTree } from "./routeTree.gen.ts"; export function getRouter() { const router = createRouter({ routeTree, defaultPreload: "intent", defaultErrorComponent: () =>
Internal Server Error
, defaultNotFoundComponent: () =>
Not Found
, scrollRestoration: true, }); return router; } ``` The router factory configures preloading behavior, scroll restoration, and default error/not-found components. ## 4. Create the Root Route The root route defines your HTML shell with head management and scripts: ```tsx [src/routes/__root.tsx] /// import { HeadContent, Link, Scripts, createRootRoute } from "@tanstack/react-router"; import { TanStackRouterDevtools } from "@tanstack/react-router-devtools"; import * as React from "react"; import appCss from "~/styles/app.css?url"; export const Route = createRootRoute({ head: () => ({ meta: [ { charSet: "utf8" }, { name: "viewport", content: "width=device-width, initial-scale=1" }, ], links: [{ rel: "stylesheet", href: appCss }], scripts: [{ src: "/customScript.js", type: "text/javascript" }], }), errorComponent: () =>

500: Internal Server Error

, notFoundComponent: () =>

404: Page Not Found

, shellComponent: RootDocument, }); function RootDocument({ children }: { children: React.ReactNode }) { return (
Home {" "} 404

{children} ); } ``` Define meta tags, stylesheets, and scripts in the `head()` function. The `shellComponent` provides the HTML document shell that wraps all pages. Use `HeadContent` to render the head configuration and `Scripts` to inject the client-side JavaScript for hydration. ## 5. Create Page Routes Page routes define your application pages: ```tsx [src/routes/index.tsx] import { createFileRoute } from "@tanstack/react-router"; export const Route = createFileRoute("/")({ component: Home }); function Home() { return (

Welcome Home!

/api/test
); } ``` ## API Routes TanStack Start supports API routes alongside page routes. Create files in `src/routes/api/` to define server endpoints that Nitro serves automatically. ## Learn More - [TanStack Start Documentation](https://tanstack.com/start){rel=""nofollow""} - [Server Entry](https://nitro.build/docs/server-entry) # SSR with Vue Router ::code-tree{expand-all default-value="app/entry-server.ts"} ```json [package.json] { "type": "module", "scripts": { "build": "vite build", "dev": "vite dev", "preview": "vite preview" }, "devDependencies": { "@vitejs/plugin-vue": "^6.0.4", "nitro": "latest", "unhead": "^2.1.3", "vite": "beta", "vite-plugin-devtools-json": "^1.0.0", "vue": "^3.5.27", "vue-router": "^4.6.4" } } ``` ```json [tsconfig.json] { "extends": "nitro/tsconfig" } ``` ```js [vite.config.mjs] import vue from "@vitejs/plugin-vue"; import { defineConfig } from "vite"; import devtoolsJson from "vite-plugin-devtools-json"; import { nitro } from "nitro/vite"; export default defineConfig((_env) => ({ plugins: [patchVueExclude(vue(), /\?assets/), devtoolsJson(), nitro()], environments: { client: { build: { rollupOptions: { input: "./app/entry-client.ts" } } }, ssr: { build: { rollupOptions: { input: "./app/entry-server.ts" } } }, }, })); // Workaround https://github.com/vitejs/vite-plugin-vue/issues/677 function patchVueExclude(plugin, exclude) { const original = plugin.transform.handler; plugin.transform.handler = function (...args) { if (exclude.test(args[1])) return; return original.call(this, ...args); }; return plugin; } ``` ```vue [app/app.vue] ``` ```ts [app/entry-client.ts] import { createSSRApp } from "vue"; import { RouterView, createRouter, createWebHistory } from "vue-router"; import { routes } from "./routes.ts"; async function main() { const app = createSSRApp(RouterView); const router = createRouter({ history: createWebHistory(), routes }); app.use(router); await router.isReady(); app.mount("#root"); } // eslint-disable-next-line unicorn/prefer-top-level-await main(); ``` ```ts [app/entry-server.ts] import { createSSRApp } from "vue"; import { renderToString } from "vue/server-renderer"; import { RouterView, createMemoryHistory, createRouter } from "vue-router"; import { createHead, transformHtmlTemplate } from "unhead/server"; import { routes } from "./routes.ts"; import clientAssets from "./entry-client.ts?assets=client"; async function handler(request: Request): Promise { const app = createSSRApp(RouterView); const router = createRouter({ history: createMemoryHistory(), routes }); app.use(router); const url = new URL(request.url); const href = url.href.slice(url.origin.length); await router.push(href); await router.isReady(); const assets = clientAssets.merge( ...(await Promise.all( router.currentRoute.value.matched .map((to) => to.meta.assets) .filter(Boolean) .map((fn) => (fn as any)().then((m: any) => m.default)) )) ); const head = createHead(); head.push({ link: [ ...assets.css.map((attrs: any) => ({ rel: "stylesheet", ...attrs })), ...assets.js.map((attrs: any) => ({ rel: "modulepreload", ...attrs })), ], script: [{ type: "module", src: clientAssets.entry }], }); const renderedApp = await renderToString(app); const html = await transformHtmlTemplate(head, htmlTemplate(renderedApp)); return new Response(html, { headers: { "Content-Type": "text/html;charset=utf-8" }, }); } function htmlTemplate(body: string): string { return /* html */ ` Vue Router Custom Framework
${body}
`; } export default { fetch: handler, }; ``` ```ts [app/routes.ts] import type { RouteRecordRaw } from "vue-router"; export const routes: RouteRecordRaw[] = [ { path: "/", name: "app", component: () => import("./app.vue"), meta: { assets: () => import("./app.vue?assets"), }, children: [ { path: "/", name: "home", component: () => import("./pages/index.vue"), meta: { assets: () => import("./pages/index.vue?assets"), }, }, { path: "/about", name: "about", component: () => import("./pages/about.vue"), meta: { assets: () => import("./pages/about.vue?assets"), }, }, { path: "/:catchAll(.*)", name: "not-found", component: () => import("./pages/not-found.vue"), meta: { assets: () => import("./pages/not-found.vue?assets"), }, }, ], }, ]; ``` ```ts [app/shims.d.ts] declare module "*.vue" { import type { DefineComponent } from "vue"; const component: DefineComponent<{}, {}, any>; export default component; } ``` ```css [app/styles.css] * { box-sizing: border-box; } body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; background: #f5f5f5; color: #333; } main { max-width: 800px; margin: 0 auto; padding: 2rem; } h1 { font-size: 2.5rem; margin-bottom: 0.5rem; } .card { background: white; border-radius: 8px; padding: 2rem; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); margin: 2rem 0; } button { background: rgb(83, 91, 242); color: white; border: none; padding: 0.5rem 1rem; border-radius: 4px; font-size: 1rem; cursor: pointer; } button:hover { background: #535bf2; } .subtitle { color: #666; font-size: 1.1rem; margin-bottom: 2rem; } ``` ```vue [app/pages/about.vue] ``` ```vue [app/pages/index.vue] ``` ```vue [app/pages/not-found.vue] ``` :: Set up server-side rendering (SSR) with Vue, Vue Router, Vite, and Nitro. This setup enables per-route code splitting, head management with unhead, and client hydration. ## Overview ::steps{level="4"} #### Add the Nitro Vite plugin to your Vite config #### Define routes with lazy-loaded components #### Create a server entry that renders your app with router support #### Create a client entry that hydrates and takes over routing #### Create page components :: ## 1. Configure Vite Add the Nitro and Vue plugins to your Vite config. Define both `client` and `ssr` environments: ```js [vite.config.mjs] import vue from "@vitejs/plugin-vue"; import { defineConfig } from "vite"; import devtoolsJson from "vite-plugin-devtools-json"; import { nitro } from "nitro/vite"; export default defineConfig((_env) => ({ plugins: [patchVueExclude(vue(), /\?assets/), devtoolsJson(), nitro()], environments: { client: { build: { rollupOptions: { input: "./app/entry-client.ts" } } }, ssr: { build: { rollupOptions: { input: "./app/entry-server.ts" } } }, }, })); // Workaround https://github.com/vitejs/vite-plugin-vue/issues/677 function patchVueExclude(plugin, exclude) { const original = plugin.transform.handler; plugin.transform.handler = function (...args) { if (exclude.test(args[1])) return; return original.call(this, ...args); }; return plugin; } ``` The `patchVueExclude` helper prevents the Vue plugin from processing asset imports (files with `?assets` query parameter). ## 2. Define Routes Create route definitions with lazy-loaded components and asset metadata: ```ts [app/routes.ts] import type { RouteRecordRaw } from "vue-router"; export const routes: RouteRecordRaw[] = [ { path: "/", name: "app", component: () => import("./app.vue"), meta: { assets: () => import("./app.vue?assets"), }, children: [ { path: "/", name: "home", component: () => import("./pages/index.vue"), meta: { assets: () => import("./pages/index.vue?assets"), }, }, { path: "/about", name: "about", component: () => import("./pages/about.vue"), meta: { assets: () => import("./pages/about.vue?assets"), }, }, { path: "/:catchAll(.*)", name: "not-found", component: () => import("./pages/not-found.vue"), meta: { assets: () => import("./pages/not-found.vue?assets"), }, }, ], }, ]; ``` Use dynamic imports for lazy-loaded components to enable code splitting. The `meta.assets` function loads route-specific CSS and JS chunks. Define child routes under a root layout component for nested routing. ## 3. Create the Server Entry The server entry renders your Vue app with router support and head management: ```ts [app/entry-server.ts] import { createSSRApp } from "vue"; import { renderToString } from "vue/server-renderer"; import { RouterView, createMemoryHistory, createRouter } from "vue-router"; import { createHead, transformHtmlTemplate } from "unhead/server"; import { routes } from "./routes.ts"; import clientAssets from "./entry-client.ts?assets=client"; async function handler(request: Request): Promise { const app = createSSRApp(RouterView); const router = createRouter({ history: createMemoryHistory(), routes }); app.use(router); const url = new URL(request.url); const href = url.href.slice(url.origin.length); await router.push(href); await router.isReady(); const assets = clientAssets.merge( ...(await Promise.all( router.currentRoute.value.matched .map((to) => to.meta.assets) .filter(Boolean) .map((fn) => (fn as any)().then((m: any) => m.default)) )) ); const head = createHead(); head.push({ link: [ ...assets.css.map((attrs: any) => ({ rel: "stylesheet", ...attrs })), ...assets.js.map((attrs: any) => ({ rel: "modulepreload", ...attrs })), ], script: [{ type: "module", src: clientAssets.entry }], }); const renderedApp = await renderToString(app); const html = await transformHtmlTemplate(head, htmlTemplate(renderedApp)); return new Response(html, { headers: { "Content-Type": "text/html;charset=utf-8" }, }); } function htmlTemplate(body: string): string { return /* html */ ` Vue Router Custom Framework
${body}
`; } export default { fetch: handler, }; ``` The server uses `createMemoryHistory()` since there's no browser URL bar—the router navigates to the requested URL before rendering. Assets are loaded dynamically based on matched routes, ensuring only the CSS and JS needed for the current page are included. The `unhead` library manages `` elements, injecting stylesheets and scripts via `transformHtmlTemplate`. ## 4. Create the Client Entry The client entry hydrates the server-rendered HTML and takes over routing: ```ts [app/entry-client.ts] import { createSSRApp } from "vue"; import { RouterView, createRouter, createWebHistory } from "vue-router"; import { routes } from "./routes.ts"; async function main() { const app = createSSRApp(RouterView); const router = createRouter({ history: createWebHistory(), routes }); app.use(router); await router.isReady(); app.mount("#root"); } // eslint-disable-next-line unicorn/prefer-top-level-await main(); ``` The client entry creates a Vue app with `createWebHistory()` for browser-based routing. After the router is ready, it mounts to the `#root` element and hydrates the server-rendered HTML. ## 5. Create the Root Component The root component provides navigation and renders child routes: ```vue [app/app.vue] ``` ## Learn More - [Vue Router Documentation](https://router.vuejs.org/){rel=""nofollow""} - [Unhead Documentation](https://unhead.unjs.io/){rel=""nofollow""} - [Renderer](https://nitro.build/docs/renderer) - [Server Entry](https://nitro.build/docs/server-entry) # Vite + tRPC ::code-tree{expand-all default-value="server/trpc.ts"} ```html [index.html] tRPC Counter
Counter
``` ```json [package.json] { "type": "module", "scripts": { "dev": "vite", "build": "vite build", "preview": "vite preview" }, "devDependencies": { "@trpc/client": "^11.9.0", "@trpc/server": "^11.9.0", "nitro": "latest", "vite": "beta", "zod": "^4.3.6" } } ``` ```json [tsconfig.json] { "extends": "nitro/tsconfig", "compilerOptions": {} } ``` ```ts [vite.config.ts] import { defineConfig } from "vite"; import { nitro } from "nitro/vite"; export default defineConfig({ plugins: [ nitro({ routes: { "/trpc/**": "./server/trpc.ts", }, }), ], }); ``` ```ts [server/trpc.ts] import { initTRPC } from "@trpc/server"; import { fetchRequestHandler } from "@trpc/server/adapters/fetch"; let counter = 0; const t = initTRPC.create(); export const appRouter = t.router({ get: t.procedure.query(() => { return { value: counter }; }), inc: t.procedure.mutation(() => { counter++; return { value: counter }; }), }); export type AppRouter = typeof appRouter; export default { async fetch(request: Request): Promise { return fetchRequestHandler({ endpoint: "/trpc", req: request, router: appRouter, }); }, }; ``` :: Set up tRPC with Vite and Nitro for end-to-end typesafe APIs without code generation. This example builds a counter with server-side rendering for the initial value and client-side updates. ## Overview ::steps{level="4"} #### Configure Vite with the Nitro plugin and route tRPC requests #### Create a tRPC router with procedures #### Create an HTML page with server-side rendering and client interactivity :: ## 1. Configure Vite Add the Nitro plugin and configure the `/trpc/**` route to point to your tRPC handler: ```ts [vite.config.ts] import { defineConfig } from "vite"; import { nitro } from "nitro/vite"; export default defineConfig({ plugins: [ nitro({ routes: { "/trpc/**": "./server/trpc.ts", }, }), ], }); ``` The `routes` option maps URL patterns to handler files. All requests to `/trpc/*` are handled by the tRPC router. ## 2. Create the tRPC Router Define your tRPC router with procedures and export it as a fetch handler: ```ts [server/trpc.ts] import { initTRPC } from "@trpc/server"; import { fetchRequestHandler } from "@trpc/server/adapters/fetch"; let counter = 0; const t = initTRPC.create(); export const appRouter = t.router({ get: t.procedure.query(() => { return { value: counter }; }), inc: t.procedure.mutation(() => { counter++; return { value: counter }; }), }); export type AppRouter = typeof appRouter; export default { async fetch(request: Request): Promise { return fetchRequestHandler({ endpoint: "/trpc", req: request, router: appRouter, }); }, }; ``` Define procedures using `t.procedure.query()` for read operations and `t.procedure.mutation()` for write operations. Export the `AppRouter` type so clients get full type inference. The default export uses tRPC's fetch adapter to handle incoming requests. ## 3. Create the HTML Page Create an HTML page with server-side rendering and client-side interactivity: ```html [index.html] tRPC Counter
Counter
``` The `

{{ message.user }}

Avatar

{{ message.text }}

{{ message.date }}

` ``` ```ts [nitro.config.ts] import { defineConfig } from "nitro"; export default defineConfig({ serverDir: "./", renderer: { static: true }, features: { websocket: true }, }); ``` ```json [package.json] { "type": "module", "scripts": { "dev": "nitro dev", "build": "nitro build" }, "devDependencies": { "nitro": "latest" } } ``` ```json [tsconfig.json] { "extends": "nitro/tsconfig" } ``` ```ts [vite.config.ts] import { defineConfig } from "vite"; import { nitro } from "nitro/vite"; export default defineConfig({ plugins: [nitro()] }); ``` ```ts [routes/_ws.ts] import { defineWebSocketHandler } from "nitro"; export default defineWebSocketHandler({ open(peer) { peer.send({ user: "server", message: `Welcome ${peer}!` }); peer.publish("chat", { user: "server", message: `${peer} joined!` }); peer.subscribe("chat"); }, message(peer, message) { if (message.text().includes("ping")) { peer.send({ user: "server", message: "pong" }); } else { const msg = { user: peer.toString(), message: message.toString(), }; peer.send(msg); // echo peer.publish("chat", msg); } }, close(peer) { peer.publish("chat", { user: "server", message: `${peer} left!` }); }, }); ``` :: This example implements a simple chat room using WebSockets. Clients connect, send messages, and receive messages from other users in real-time. The server broadcasts messages to all connected clients using pub/sub channels. ## WebSocket Handler Create a WebSocket route using `defineWebSocketHandler`. ```ts [routes/_ws.ts] import { defineWebSocketHandler } from "nitro"; export default defineWebSocketHandler({ open(peer) { peer.send({ user: "server", message: `Welcome ${peer}!` }); peer.publish("chat", { user: "server", message: `${peer} joined!` }); peer.subscribe("chat"); }, message(peer, message) { if (message.text().includes("ping")) { peer.send({ user: "server", message: "pong" }); } else { const msg = { user: peer.toString(), message: message.toString(), }; peer.send(msg); // echo peer.publish("chat", msg); } }, close(peer) { peer.publish("chat", { user: "server", message: `${peer} left!` }); }, }); ``` Different hooks are exposed by `defineWebSocketHandler()` to integrate with different parts of the websocket lifecycle. ## Learn More - [Routing](https://nitro.build/docs/routing) - [crossws Documentation](https://crossws.h3.dev/guide/hooks){rel=""nofollow""} # Nitro v3 Beta is here! ## A Brief History Nitro started as the server engine for [Nuxt 3](https://nuxt.com){rel=""nofollow""}, designed to solve a specific problem: deployment-agnostic servers. Over time, Nitro grew beyond Nuxt. It became the foundation for many meta-frameworks and a toolkit for building standalone servers. With Nitro v3, we took the opportunity to rethink the fundamentals. leaner APIs, Web standards, first-class [Rolldown](https://rolldown.rs/){rel=""nofollow""} and [Vite v8](https://vite.dev/){rel=""nofollow""} integration, and a better experience for both humans and agents (more on that later!) Since we quietly announced v3 [alpha.0](https://github.com/nitrojs/nitro/releases/tag/v3.0.1-alpha.0){rel=""nofollow""} (11 Oct 2025) at the first [Vite Conf](https://viteconf.amsterdam/){rel=""nofollow""}, Nitro v3 has been adopted by many users ([\~280k](https://npmtrends.com/nitro-vs-nitro-nightly){rel=""nofollow""} weekly downloads!) and refined through amazing contributions and feedback. including [Tanstack Start](https://tanstack.com/start/latest/docs/framework/react/guide/hosting#nitro){rel=""nofollow""}, [Vercel Workflows](https://useworkflow.dev/docs/getting-started){rel=""nofollow""}, and production apps like [T3Chat](https://t3.chat/){rel=""nofollow""}. A huge thanks to the VoidZero (Vite and Rolldown), Nuxt ([v5 is coming!](https://nitro.build/#nuxt-v5)) and TanStack Start teams and every contributor who helped bring Nitro v3 to this milestone. ❤️ ## Why Build Servers? We don't ship raw source files to the browser. We use build tools because they solve real problems: **HMR** for instant feedback, **code splitting** to load only what a route needs, **tree shaking** to eliminate dead code, and **minification** for smaller payloads. Tools like Webpack and then [Vite](https://vite.dev/){rel=""nofollow""} transformed frontend development from painful to productive. But frontend apps don't exist in isolation, they need APIs, databases, authentication, real-time data. They need a server. With the rise of serverless and edge computing, the server side now faces the same constraints the frontend solved years ago. **Cold starts** mean every millisecond of startup matters. **Memory limits** are strict — bloated dependencies can push you over. **Bundle size** directly impacts deploy speed and boot time. And your code needs to run everywhere: Node.js, Deno, Bun, Cloudflare Workers, Vercel, etc. Yet most server frameworks still ship unoptimized, unbundled code, assuming a long-running process where none of this matters. Nitro brings the build-tool philosophy to the backend. The same great DX you expect from frontend tooling: HMR for fast iteration and optimized builds powered by Rolldown with tree-shaken production output that performs as close to bare-metal as possible. **One codebase, any runtime, any platform.** ## ⚡ First-Class Vite Integration Nitro now has a native [Vite](https://vite.dev){rel=""nofollow""} plugin to build full stack apps. ```ts [vite.config.ts] import { defineConfig } from "vite"; import { nitro } from "nitro/vite"; export default defineConfig({ plugins: [nitro()], }); ``` Adding `nitro()` to your Vite apps gives you: - **API routes** via filesystem routing - **Server-side rendering** integrated with your frontend build - **A production server** — a single `vite build` produces an optimized `.output/` folder with both frontend and backend, ready to deploy This means you can add a full backend to any Vite project — See [examples](https://nitro.build/examples) with [React](https://nitro.build/examples/vite-ssr-react), [Vue](https://nitro.build/examples/vite-ssr-vue-router) and [Solid.js](https://nitro.build/examples/vite-ssr-solid). ## 🚀 Performance by Default, Zero Bloat Nitro compiles your routes at build time. There is no runtime router — each route loads on demand. Only the code needed to handle a specific request is loaded and executed. Minimal server bundle built with the `standard` preset is less than `10kB`, can be served with [srvx](https://srvx.h3.dev/){rel=""nofollow""} at close to native speeds, and includes all the good features from [H3](https://h3.dev/){rel=""nofollow""}. We have also significantly reduced the number of dependencies, down to [less than 20](https://npmgraph.js.org/?q=nitro-nightly){rel=""nofollow""} from [321 dependencies](https://npmgraph.js.org/?q=nitropack){rel=""nofollow""}. ## 🖌️ New Identity: `nitro` Nitro v3 ships under a new NPM package: [`nitro`](https://npmx.dev/package/nitro){rel=""nofollow""}, replacing the legacy `nitropack`. All imports now use clean `nitro/*` subpaths: ```ts import { defineNitroConfig } from "nitro/config"; import { defineHandler } from "nitro"; import { useStorage } from "nitro/storage"; import { useDatabase } from "nitro/database"; ``` No more deep `nitropack/runtime/*` paths, plus, you can import nitro subpaths outside of builder useful for unit testing. ## 🔧 Bring Your Own Framework Nitro v3 is not opinionated about your HTTP layer. You can use the built-in filesystem routing, or take full control with a `server.ts` entry file and bring any framework you prefer: ```ts [server.ts] import { Hono } from "hono"; const app = new Hono(); app.get("/", (c) => c.text("Hello from Hono!")); export default app; ``` ## 🌐 H3 (v2) with Web Standards Nitro v3 upgrades to [H3 v2](https://h3.dev){rel=""nofollow""}, which has been fully rewritten around web standard primitives — [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request){rel=""nofollow""}, [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response){rel=""nofollow""}, [`Headers`](https://developer.mozilla.org/en-US/docs/Web/API/Headers){rel=""nofollow""}, and [`URL`](https://developer.mozilla.org/en-US/docs/Web/API/URL){rel=""nofollow""}. The result is cleaner, more portable server code: ```ts [routes/hello.ts] import { defineHandler } from "nitro"; export default defineHandler((event) => { const ua = event.req.headers.get("user-agent"); return { message: "Hello Nitro v3!", ua }; }); ``` Reading request bodies uses native APIs: ```ts [routes/submit.ts] import { defineHandler } from "nitro"; export default defineHandler(async (event) => { const body = await event.req.json(); return { received: body }; }); ``` No wrappers, no abstractions for things the platform already provides. If you know the Web API, you know H3 v2. [Elysia](https://elysiajs.com/){rel=""nofollow""}, [h3](https://h3.dev){rel=""nofollow""}, [Hono](https://hono.dev){rel=""nofollow""} — anything that speaks web standards works with Nitro. ## 🗄️ Built-in Primitives Nitro ships with powerful but small and **fully opt-in** agnostic server primitives that work across every runtime. ::note When not used, nothing extra will be added to the server bundle. You can still use native platform primitives alongside Nitro's built-in ones. We are also bringing first class emulation for platform-specific primitives for dev See [env-runner](https://github.com/unjs/env-runner){rel=""nofollow""} and [nitrojs/nitro#4088](https://github.com/nitrojs/nitro/pull/4088){rel=""nofollow""} for more details. :: ### Storage A runtime-agnostic key-value layer with 20+ drivers — FS, Redis, S3, Cloudflare KV, Vercel Blob and [more](https://unstorage.unjs.io/drivers){rel=""nofollow""}. Attach drivers to namespaces and swap them without changing your application code. ```ts import { useStorage } from "nitro/storage"; const storage = useStorage(); await storage.setItem("user:1", { name: "Nitro" }); ``` :read-more{to="https://nitro.build/docs/storage"} ### Caching Cache server routes and functions, backed by the storage layer. Supports stale-while-revalidate, TTL, and custom cache keys out of the box. ```ts import { defineCachedHandler } from "nitro/cache"; export default defineCachedHandler((event) => { return "I am cached for an hour"; }, { maxAge: 60 * 60 }); ``` :read-more{to="https://nitro.build/docs/cache"} ### Database A built-in SQL database that defaults to SQLite for development and can connect to Postgres, MySQL, and [more](https://db0.unjs.io/connectors){rel=""nofollow""} using the same API. ```ts import { useDatabase } from "nitro/database"; const db = useDatabase(); const users = await db.sql`SELECT * FROM users`; ``` :read-more{to="https://nitro.build/docs/database"} ## 🌍 Deploy Anywhere Build your server into an optimized `.output/` folder compatible with: - **Runtimes**: Node.js, Bun, Deno - **Platforms**: Cloudflare Workers, Netlify, Vercel, AWS Lambda, Azure, Firebase, Deno Deploy, and more No configuration needed — Nitro auto-detects your deployment target. Take advantage of platform features like ISR, SWR, and edge rendering without changing a single line of code. ## 🎨 Server-Side Rendering Render HTML with your favorite templating engine, or use component libraries like React, Vue, or Svelte directly on the server. Go full universal rendering with client-side hydration. Nitro provides the foundation and a progressive approach — start with API routes, add rendering when you need it, and scale to full SSR at your own pace. :read-more{to="https://nitro.build/docs/renderer"} ## 🟢 Nuxt v5 Nitro v3 will power the next major version of [Nuxt](https://nuxt.com){rel=""nofollow""}. [Nuxt v5](https://nuxt.com/blog/roadmap-v4){rel=""nofollow""} will ship with Nitro v3 and H3 v2 at its core, bringing web-standard request handling, Rolldown-powered builds, and the Vite Environment API to the Nuxt ecosystem. If you're a Nuxt user, you can already start preparing by familiarizing yourself with Nitro v3's new APIs, which will carry directly into Nuxt 5, and you can [follow progress](https://github.com/nuxt/nuxt/discussions/34504){rel=""nofollow""} on adopting Nitro v3 in Nuxt ## 🏁 Getting Started ### Create a New Project :pm-x{command="create-nitro-app"} See the [quick start guide](https://nitro.build/docs/quick-start) for a full step-by-step walkthrough. ## 🔄 Migrating from v2 Nitro v3 introduces intentional breaking changes to set a cleaner foundation. Here are the key ones: - `nitropack` → `nitro` (package rename) - `nitropack/runtime/*` → `nitro/*` (clean subpath imports) - `eventHandler` → `defineHandler` (H3 v2) - `createError` → `HTTPError` (H3 v2) - Web standard `event.req` headers and body APIs - Node.js minimum version: **20** - Preset renames and consolidation (e.g., `cloudflare` → `cloudflare_module`) For a complete list, see the [migration guide](https://nitro.build/docs/migration). --- Thank you to everyone who has contributed to Nitro over the years. We can't wait to see what you build with the new Nitro! ❤️ - [GitHub](https://github.com/nitrojs/nitro){rel=""nofollow""} — Issues and discussions - [Discord](https://discord.nitro.build){rel=""nofollow""} — Chat with the community # Blog Nitro blog posts. # Build Full-Stack Servers ::u-page-hero --- orientation: horizontal --- :::code-group ::::prose-pre{filename="vite.config.ts"} ```ts import { defineConfig } from 'vite' import { nitro } from 'nitro/vite' export default defineConfig({ plugins: [nitro()], nitro: { serverDir: "./server" } }) ``` :::: ::::prose-pre{filename="nitro.config.ts"} ```ts import { defineConfig } from 'nitro' export default defineConfig({ preset: "node", serverDir: "./server", routeRules: { "/api/**": { cache: true } } }) ``` :::: ::: :hero-background #title Build [/Servers]{.text-primary} #description Nitro extends your Vite application with a production-ready server, compatible with any runtime. Add server routes to your application and deploy many hosting platform with a zero-config experience. #links :app-hero-links :: ::hero-features --- features: - title: Fast description: Enjoy the fast Vite 8 (rolldown powered) development experience with HMR on the server and optimized for production. icon: i-lucide-zap color: text-amber-500 bgColor: bg-amber-500/10 borderColor: group-hover:border-amber-500/30 - title: Agnostic description: Deploy the same codebase to any deployment provider with zero config and locked-in. icon: i-lucide-globe color: text-sky-500 bgColor: bg-sky-500/10 borderColor: group-hover:border-sky-500/30 - title: Minimal description: Nitro adds no overhead to runtime. Build your servers with any modern tool you like. icon: i-lucide-feather color: text-emerald-500 bgColor: bg-emerald-500/10 borderColor: group-hover:border-emerald-500/30 --- :: ::performance-showcase --- metrics: - label: Bare metal perf value: ~Native unit: RPS description: Using compile router, and fast paths for request handling. icon: i-lucide-gauge color: text-emerald-500 bgColor: bg-emerald-500/10 barWidth: 95% barColor: bg-emerald-500 - label: Minimum install Size value: Tiny unit: deps description: Minimal dependencies. No bloated node_modules. icon: i-lucide-package color: text-sky-500 bgColor: bg-sky-500/10 barWidth: 15% barColor: bg-sky-500 - label: Small and portable output value: ‹ 10 unit: kB description: Standard server builds produce ultra-small output bundles. icon: i-lucide-file-output color: text-violet-500 bgColor: bg-violet-500/10 barWidth: 10% barColor: bg-violet-500 - label: FAST builds value: ‹ 1 unit: sec description: Cold production builds complete in seconds, not minutes. icon: i-lucide-timer color: text-amber-500 bgColor: bg-amber-500/10 barWidth: 12% barColor: bg-amber-500 --- :: ::landing-features #body :::feature-card --- headline: Routing link: /docs/routing link-label: Routing docs --- #title File-system routing #description Create server routes in the routes/ folder and they are automatically registered. Or bring your own framework — H3, Hono, Elysia, Express — via a server.ts entry. ::: :::feature-card --- headline: Versatile link: /deploy link-label: Explore deploy targets --- #title Deploy everywhere #description The same codebase deploys to Node.js, Cloudflare Workers, Deno, Bun, AWS Lambda, Vercel, Netlify, and more — zero config, no vendor lock-in. ::: :::feature-card --- headline: Storage link: /docs/storage link-label: Storage docs --- #title Universal storage #description Built-in key-value storage abstraction powered by unstorage. Works with filesystem, Redis, Cloudflare KV, and more — same API everywhere. ::: :::feature-card --- headline: Caching link: /docs/cache link-label: Caching docs --- #title Built-in caching #description Cache route handlers and arbitrary functions with a simple API. Supports multiple storage backends and stale-while-revalidate patterns. ::: :::feature-card --- headline: Server Entry link: /docs/server-entry link-label: Server entry docs --- #title Web standard server #description Go full Web standard and pick the library of your choice. Use H3, Hono, Elysia, Express, or the raw fetch API — Nitro handles the rest. ::: :::feature-card --- headline: Renderer link: /docs/renderer link-label: Renderer docs --- #title Universal renderer #description Use any frontend framework as your renderer. Nitro provides the server layer while your framework handles the UI. ::: :::feature-card --- headline: Plugins link: /docs/plugins link-label: Plugins docs --- #title Server plugins #description Extend Nitro's runtime behavior with plugins. Hook into lifecycle events, register custom logic, and auto-load from the plugins/ directory. ::: :::feature-card --- headline: Database link: /docs/database link-label: Database docs --- #title Built-in database #description Lightweight SQL database layer powered by db0. Pre-configured with SQLite out of the box, with support for PostgreSQL, MySQL, and Cloudflare D1. ::: :::feature-card --- headline: Assets link: /docs/assets link-label: Assets docs --- #title Static & server assets #description Serve public assets directly to clients or bundle server assets for programmatic access. Works seamlessly across all deployment targets. ::: :: :page-sponsors