Nitro Renderer

Use a renderer to handle all unmatched routes with custom HTML or a templating system.
Nitro v3 Alpha docs are a work in progress — expect updates, rough edges, and occasional inaccuracies.

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.

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.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>My Vite + Nitro App</title>
  </head>
  <body>
    <div id="app"></div>
    <script type="module" src="/src/main.ts"></script>
  </body>
</html>
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.

import { defineNitroConfig } from "nitro/config";

export default defineNitroConfig({
  renderer: {
    template: './app.html'
  }
})

Hypertext Preprocessor (experimental)

Nitro uses rendu Hypertext Preprocessor, which provides a simple and powerful way to create dynamic HTML templates with JavaScript expressions.

You can use special delimiters to inject dynamic content:

  • {{ content }} to output HTML-escaped content
  • {{{ content }}} or <?= expression ?> to output raw (unescaped) content
  • <? ... ?> for JavaScript control flow

It also exposes global variables:

  • $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
index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Dynamic template</title>
  </head>
  <body>
    <h1>Hello {{ $REQUEST.url }}</h1>
  </body>
</html>
Read more in Rendu Documentation.

Custom renderer handler

For more complex scenarios, you can create a custom renderer handler that programmatically generates responses.

Create a renderer file and use defineRenderHandler to define your custom rendering logic:

renderer.ts
import { defineRenderHandler } from "nitro/runtime";

export default defineRenderHandler((event) => {
  return {
    body: `<!DOCTYPE html>
      <html>
        <head>
          <title>Custom Renderer</title>
        </head>
        <body>
          <h1>Hello from custom renderer!</h1>
          <p>Current path: ${event.path}</p>
        </body>
      </html>`,
    headers: {
      'content-type': 'text/html; charset=utf-8'
    }
  }
})

Then, specify the renderer entry in the Nitro config:

nitro.config.ts
import { defineNitroConfig } from "nitro/config";

export default defineNitroConfig({
  renderer: {
    entry: './renderer.ts'
  }
})

Renderer priority

The renderer always acts as a catch-all route (/**) and has the lowest priority. This means:

Specific API routes are matched first (e.g., /api/users)

Specific server routes are matched next (e.g., /about)

The renderer catches everything else

api/
  users.ts        → /api/users (matched first)
routes/
  about.ts        → /about (matched second)
renderer.ts         → /** (catches all other routes)
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 in Architecture > Request lifecycle.

Use Cases

Single-Page Application (SPA)

Serve your SPA's index.html for all routes to enable client-side routing:

This is the default behavior of Nitro when used with Vite.