VercelVercel
Menu

Vite + Nitro on Vercel

Last updated March 9, 2026

Nitro is a universal server toolkit that adds server-side rendering (SSR), API routes, server middleware and other backend capabiltiies to any Vite application. It powers frameworks like Nuxt and deploys to Vercel with zero configuration.

By adding Nitro to your existing Vite project, you get:

  • Server-Side Rendering (SSR): Render pages dynamically on the server for improved SEO and faster initial page loads
  • API Routes: Create backend endpoints using file-based routing in the api/ or routes/ directory
  • Vercel Functions: Your server routes automatically become Vercel Functions with Fluid compute

To add server capabilities to an existing Vite project, install the nitro package:

Terminal
pnpm i nitro

Then add the Nitro Vite plugin to your configuration:

vite.config.ts
import { defineConfig } from 'vite';
import { nitro } from 'nitro/vite';
 
export default defineConfig({
  plugins: [nitro()],
});

Nitro supports file-based routing in the api/ or routes/ directory. Each file becomes an API endpoint based on its path.

Create a file in the api/ directory to define a route:

api/hello.ts
import { defineHandler } from 'nitro/h3';
 
export default defineHandler(() => 'Hello from the server!');

This creates a GET /api/hello endpoint.

Use square brackets [param] for dynamic URL segments. Access params via event.context.params:

api/users/[id].ts
import { defineHandler } from 'nitro/h3';
 
export default defineHandler((event) => {
  const { id } = event.context.params!;
  return { userId: id };
});

This creates a GET /api/users/:id endpoint (e.g., /api/users/123).

Suffix your file with the HTTP method (.get.ts, .post.ts, .put.ts, .delete.ts):

api/users.post.ts
import { defineHandler } from 'nitro/h3';
 
export default defineHandler(async (event) => {
  const body = await event.req.json();
  return { message: 'User created', data: body };
});

When you deploy a Vite + Nitro app to Vercel, your server routes automatically become Vercel Functions with Fluid compute enabled by default.

Vercel Functions scale based on traffic demands, preventing failures during peak hours while minimizing costs during periods of low activity.

With Nitro on Vercel, you get:

  • Scaling to zero when not in use
  • Automatic scaling with traffic increases
  • Support for standard Web APIs, such as URLPattern, Response, and more

Learn more about Vercel Functions

Nitro enables SSR for any Vite app with minimal configuration. The setup varies by UI framework.

  1. Install the required dependencies:
Terminal
pnpm i nitro react react-dom @vitejs/plugin-react
  1. Update your Vite config to add the Nitro and React plugins:
vite.config.ts
import { defineConfig } from 'vite';
import { nitro } from 'nitro/vite';
import react from '@vitejs/plugin-react';
 
export default defineConfig({
  plugins: [nitro(), react()],
});
  1. Create the shared app component:
src/app.tsx
import { useState } from 'react';
 
export function App() {
  const [count, setCount] = useState(0);
  return (
    <>
      <h1>Vite + Nitro + React</h1>
      <button onClick={() => setCount((c) => c + 1)}>Count is {count}</button>
    </>
  );
}
  1. Create the client entry file that handles hydration:
src/entry-client.tsx
import '@vitejs/plugin-react/preamble';
import { hydrateRoot } from 'react-dom/client';
import { App } from './app.tsx';
 
hydrateRoot(document.querySelector('#app')!, <App />);
  1. Create the server entry file that renders your app to HTML:
src/entry-server.tsx
import './styles.css';
import { renderToReadableStream } from 'react-dom/server.edge';
import { App } from './app.tsx';
 
import clientAssets from './entry-client?assets=client';
import serverAssets from './entry-server?assets=ssr';
 
export default {
  async fetch(_req: Request) {
    const assets = clientAssets.merge(serverAssets);
    return new Response(
      await renderToReadableStream(
        <html lang="en">
          <head>
            <meta
              name="viewport"
              content="width=device-width, initial-scale=1.0"
            />
            {assets.css.map((attr: any) => (
              <link key={attr.href} rel="stylesheet" {...attr} />
            ))}
            {assets.js.map((attr: any) => (
              <link key={attr.href} rel="modulepreload" {...attr} />
            ))}
            <script type="module" src={assets.entry} />
          </head>
          <body id="app">
            <App />
          </body>
        </html>,
      ),
      { headers: { 'Content-Type': 'text/html;charset=utf-8' } },
    );
  },
};
  1. Update your TypeScript config:
tsconfig.json
{
  "extends": "nitro/tsconfig",
  "compilerOptions": {
    "jsx": "react-jsx",
    "jsxImportSource": "react"
  }
}
  1. Install the required dependencies:
Terminal
pnpm i nitro vue vue-router @vitejs/plugin-vue
  1. Update your Vite config to add the Nitro and Vue plugins:
vite.config.ts
import { defineConfig } from 'vite';
import { nitro } from 'nitro/vite';
import vue from '@vitejs/plugin-vue';
 
export default defineConfig({
  plugins: [vue(), nitro()],
});
  1. Define your routes with lazy-loaded components:
app/routes.ts
import type { RouteRecordRaw } from 'vue-router';
 
export const routes: RouteRecordRaw[] = [
  {
    path: '/',
    name: 'home',
    component: () => import('./pages/index.vue'),
  },
  {
    path: '/about',
    name: 'about',
    component: () => import('./pages/about.vue'),
  },
];
  1. Create the client entry file:
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');
}
 
main();
  1. Create the server entry file:
app/entry-server.ts
import { createSSRApp } from 'vue';
import { renderToString } from 'vue/server-renderer';
import { RouterView, createMemoryHistory, createRouter } from 'vue-router';
import { routes } from './routes.ts';
 
import clientAssets from './entry-client.ts?assets=client';
 
export default {
  async fetch(request: Request): Promise<Response> {
    const app = createSSRApp(RouterView);
    const router = createRouter({ history: createMemoryHistory(), routes });
    app.use(router);
 
    const url = new URL(request.url);
    await router.push(url.href.slice(url.origin.length));
    await router.isReady();
 
    const renderedApp = await renderToString(app);
    const html = `<!DOCTYPE html>
<html lang="en"><head>
  ${clientAssets.css.map((a: any) => `<link rel="stylesheet" href="${a.href}" />`).join('\n')}
  <script type="module" src="${clientAssets.entry}"></script>
</head><body><div id="root">${renderedApp}</div></body></html>`;
 
    return new Response(html, {
      headers: { 'Content-Type': 'text/html;charset=utf-8' },
    });
  },
};
  1. Update your TypeScript config:
tsconfig.json
{
  "extends": "nitro/tsconfig"
}
  1. Install the required dependencies:
Terminal
pnpm i nitro preact preact-render-to-string @preact/preset-vite
  1. Update your Vite config to add the Nitro and Preact plugins:
vite.config.ts
import { defineConfig } from 'vite';
import { nitro } from 'nitro/vite';
import preact from '@preact/preset-vite';
 
export default defineConfig({
  plugins: [nitro(), preact()],
});
  1. Create the shared app component:
src/app.tsx
import { useState } from 'preact/hooks';
 
export function App() {
  const [count, setCount] = useState(0);
  return (
    <button onClick={() => setCount((c) => c + 1)}>Count is {count}</button>
  );
}
  1. Create the client entry file:
src/entry-client.tsx
import { hydrate } from 'preact';
import { App } from './app.tsx';
 
hydrate(<App />, document.querySelector('#app')!);
  1. Create the server entry file:
src/entry-server.tsx
import './styles.css';
import { renderToReadableStream } from 'preact-render-to-string/stream';
import { App } from './app.tsx';
 
import clientAssets from './entry-client?assets=client';
import serverAssets from './entry-server?assets=ssr';
 
export default {
  async fetch(_req: Request) {
    const assets = clientAssets.merge(serverAssets);
    return new Response(
      renderToReadableStream(
        <html lang="en">
          <head>
            <meta
              name="viewport"
              content="width=device-width, initial-scale=1.0"
            />
            {assets.css.map((attr: any) => (
              <link key={attr.href} rel="stylesheet" {...attr} />
            ))}
            {assets.js.map((attr: any) => (
              <link key={attr.href} rel="modulepreload" {...attr} />
            ))}
            <script type="module" src={assets.entry} />
          </head>
          <body>
            <div id="app">
              <App />
            </div>
          </body>
        </html>,
      ),
      { headers: { 'Content-Type': 'text/html;charset=utf-8' } },
    );
  },
};
  1. Update your TypeScript config:
tsconfig.json
{
  "extends": "nitro/tsconfig",
  "compilerOptions": {
    "jsx": "react-jsx",
    "jsxImportSource": "preact"
  }
}
  1. Install the required dependencies:
Terminal
pnpm i nitro solid-js vite-plugin-solid
  1. Update your Vite config to add the Nitro and Solid plugins, and configure JSX for Solid:
vite.config.ts
import { defineConfig } from 'vite';
import { nitro } from 'nitro/vite';
import solid from 'vite-plugin-solid';
 
export default defineConfig({
  plugins: [solid({ ssr: true }), nitro()],
  esbuild: { jsx: 'preserve', jsxImportSource: 'solid-js' },
});
  1. Create the shared app component:
src/app.tsx
import { createSignal } from 'solid-js';
 
export function App() {
  const [count, setCount] = createSignal(0);
  return (
    <div>
      <h1>Hello, Solid!</h1>
      <button onClick={() => setCount((c) => c + 1)}>Count: {count()}</button>
    </div>
  );
}
  1. Create the client entry file:
src/entry-client.tsx
import { hydrate } from 'solid-js/web';
import './styles.css';
import { App } from './app.tsx';
 
hydrate(() => <App />, document.querySelector('#app')!);
  1. Create the server entry file. Solid requires two-phase rendering with HydrationScript:
src/entry-server.tsx
import { renderToStringAsync, HydrationScript } from 'solid-js/web';
import { App } from './app.tsx';
 
import clientAssets from './entry-client?assets=client';
import serverAssets from './entry-server?assets=ssr';
 
export default {
  async fetch(_req: Request): Promise<Response> {
    const appHTML = await renderToStringAsync(() => <App />);
    const rootHTML = await renderToStringAsync(() => (
      <Root appHTML={appHTML} />
    ));
    return new Response(rootHTML, {
      headers: { 'Content-Type': 'text/html' },
    });
  },
};
 
function Root(props: { appHTML?: string }) {
  const assets = clientAssets.merge(serverAssets);
  return (
    <html lang="en">
      <head>
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        {assets.css.map((attr: any) => (
          <link rel="stylesheet" {...attr} />
        ))}
        {assets.js.map((attr: any) => (
          <link rel="modulepreload" {...attr} />
        ))}
      </head>
      <body>
        <div id="app" innerHTML={props.appHTML || ''} />
        <HydrationScript />
        <script type="module" src={assets.entry} />
      </body>
    </html>
  );
}
  1. Update your TypeScript config:
tsconfig.json
{
  "extends": "nitro/tsconfig",
  "compilerOptions": {
    "jsx": "preserve",
    "jsxImportSource": "solid-js"
  }
}

For SSR without a UI framework, use an index.html file with an SSR outlet placeholder.

  1. Install Nitro:
Terminal
pnpm i nitro
  1. Update your Vite config:
vite.config.ts
import { defineConfig } from 'vite';
import { nitro } from 'nitro/vite';
 
export default defineConfig({
  plugins: [nitro()],
});
  1. Create an index.html file with an <!--ssr-outlet--> placeholder:
index.html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>My App</title>
  </head>
  <body>
    <div id="app"><!--ssr-outlet--></div>
  </body>
</html>
  1. Create the server entry file that returns content to inject into the SSR outlet:
app/entry-server.ts
export default {
  async fetch(_req: Request) {
    const content = '<p>Hello from the server!</p>';
    return new Response(content, {
      headers: { 'Content-Type': 'text/html;charset=utf-8' },
    });
  },
};

The content returned by the server entry replaces the <!--ssr-outlet--> placeholder in your HTML.

Incremental Static Regeneration (ISR) allows you to create or update content without redeploying your site. ISR has two main benefits for developers: better performance and faster build times.

To enable ISR for a Nitro route, add a routeRules option to your Nitro configuration:

nitro.config.ts
import { defineNitroConfig } from 'nitro/config';
 
export default defineNitroConfig({
  routeRules: {
    // All routes revalidate every 60 seconds in the background
    '/**': { isr: 60 },
    // This route is generated on demand and cached permanently
    '/static': { isr: true },
    // This route is prerendered at build time and cached permanently
    '/prerendered': { prerender: true },
    // This route is always fresh
    '/dynamic': { isr: false },
  },
});

Pass an options object to the isr route rule to configure caching behavior:

  • expiration: Time in seconds before the cached page regenerates by invoking the function. Set to false (or use isr: true) to cache permanently.
  • allowQuery: List of query string parameter names cached independently. An empty array ignores query values. When undefined, each unique query value is cached independently.
  • passQuery: When true, the query string is passed to the invoked function. The allowQuery filter still applies.
nitro.config.ts
import { defineNitroConfig } from 'nitro/config';
 
export default defineNitroConfig({
  routeRules: {
    '/products/**': {
      isr: {
        expiration: 60,
        allowQuery: ['q'],
        passQuery: true,
      },
    },
  },
});

On-demand revalidation lets you purge the cache for an ISR route at any time, instead of waiting for the expiration interval.

To enable on-demand revalidation:

  1. Create an environment variable to store a revalidation secret. Use the command openssl rand -base64 32 to generate a random value.
  2. Add the bypassToken to your Nitro configuration:
nitro.config.ts
import { defineNitroConfig } from 'nitro/config';
 
export default defineNitroConfig({
  vercel: {
    config: {
      bypassToken: process.env.VERCEL_BYPASS_TOKEN,
    },
  },
});
  1. Send a GET or HEAD request to the route with an x-prerender-revalidate header set to your bypassToken value. The cache revalidates immediately, and the next request returns a fresh response.

Using ISR with Vite + Nitro on Vercel offers:

  • Better performance with the global CDN
  • Zero-downtime rollouts to previously statically generated pages
  • Global content updates in 300ms
  • Generated pages are both cached and persisted to durable storage

Learn more about ISR

Vercel provides a set of System Environment Variables that are automatically available to your project.

To make environment variables accessible in Nitro server code, prefix the variable name with NITRO_ and define it in your Nitro configuration. For example, NITRO_API_TOKEN is accessible as useRuntimeConfig().apiToken.

nitro.config.ts
import { defineNitroConfig } from 'nitro/config';
 
export default defineNitroConfig({
  runtimeConfig: {
    apiToken: 'dev_token', // `dev_token` is the default value
  },
});

In Nitro server code, access environment variables using useRuntimeConfig().

api/config.ts
import { defineHandler } from 'nitro/h3';
import { useRuntimeConfig } from 'nitro/runtime-config';
 
export default defineHandler((event) => {
  return useRuntimeConfig().apiToken; // Returns `dev_token`
});

Learn more about Nitro runtime configuration

Vercel provides built-in observability for your Nitro applications, giving you visibility into your application's performance and behavior in production. Monitor function invocations, track errors, analyze latency, and inspect logs directly from the Vercel dashboard.

Learn more about Observability on Vercel

See our Frameworks documentation page to learn about the benefits available to all frameworks when you deploy on Vercel.

Learn more about deploying Vite + Nitro projects on Vercel:


Was this helpful?

supported.