Frameworks with server rendering are built on these APIs, and debugging them requires understanding them.
For server-side rendering, the server must execute your app code (JSX, TS, CSS imports) in Node. Vite supports this in two phases:
Development: create the Vite server in middleware mode inside your own Node server (Express, Hono, etc.). For each request, load the server entry through Vite, which transforms it on the fly with HMR, render HTML, and pass it through vite.transformIndexHtml so client scripts and HMR are injected. The classic API is vite.ssrLoadModule(url).
Production: build twice, vite build for the client and vite build --ssr src/entry-server.js for the server, then import the server bundle directly with no Vite at runtime. ssr.noExternal controls which dependencies are bundled into the server build; by default node_modules stay external.
The Environment API (introduced in Vite 6, evolving since) generalizes this. A Vite server or build now has several named environments, by default client and ssr, each with its own module graph, resolve conditions and plugin pipeline. Custom environments target edge runtimes or workers. Code runs in an environment through a module runner, which can live in the same Node process or in another runtime (workerd, Deno). Frameworks use this to run server code in a production-like runtime during development.
Application authors rarely touch these APIs directly; they come through frameworks. Plugin authors must be environment-aware (e.g. this.environment in hooks).
Example
// server.js (development): Express + Vite in middleware mode
import express from "express";
import fs from "node:fs/promises";
import { createServer } from "vite";
const app = express();
const vite = await createServer({ server: { middlewareMode: true }, appType: "custom" });
app.use(vite.middlewares); // assets, HMR, transforms
app.use("*all", async (req, res) => {
let html = await fs.readFile("index.html", "utf8");
html = await vite.transformIndexHtml(req.originalUrl, html);
const { render } = await vite.ssrLoadModule("/src/entry-server.js");
const appHtml = await render(req.originalUrl);
res.type("html").send(html.replace("<!--app-html-->", appHtml));
});
app.listen(5173);
// production: npx vite build && npx vite build --ssr src/entry-server.js --outDir dist/server
// then import("./dist/server/entry-server.js") without ViteRemember
- Dev SSR: Vite in middleware mode loads server code with HMR
- Prod SSR: separate client and --ssr builds, no Vite at runtime
- Environments: client, ssr and custom, each with its own module graph
- Module runners execute environment code, possibly in other runtimes
Watch out for
- Shipping the dev middleware setup to production instead of the built server bundle.