Development
Starting Development
pnpm start:devThis runs both Vite and NestJS concurrently with full HMR support.
View edits without restarts
Projects created by init run Nest through nestjs-ssr dev:
"dev:nest": "NODE_ENV=development nestjs-ssr dev --watchAssets"It compiles like nest start --watch, but restarts Nest only when compiled output outside a views directory changes. Edit a view and the server renders the new version on the next request (and Vite hot-updates the open page) without a restart; on the example app the open page shows the edit in about 0.2 s. Edit a controller or service and Nest restarts as usual.
The server finds the edited view by its component name, so the file can be called anything: SpecialsList in specials/views/recipe-list.tsx works as well as RecipeList in recipe-list.tsx. Renaming a component keeps working too. Two cases need a Nest restart to reach server rendering, and the server logs a warning once when it meets one:
- a view whose default export has no name (
export default () => …); - two views whose components share a name.
Give each page component a unique name and neither applies.
Errors while rendering
With showErrorPage: true (set by init), a page that throws answers with an error page instead of a JSON 500. In development it shows the error, the failing source lines, the stack with your frames first, the request, and links that open the file in your editor. Production shows the generic error page without any details.
How It Works
- Vite dev server runs on port 5173 (client assets, HMR)
- NestJS server runs on port 3000 (SSR, API)
- NestJS proxies asset requests (
/src/,/@,/node_modules/) and Vite's HMR WebSocket to Vite - Component changes hot-reload instantly
HMR WebSocket
Your pages are served by NestJS on :3000, but Vite's HMR client derives its WebSocket URL from the page origin. Point it at Vite explicitly:
// vite.config.ts
server: {
port: 5173,
strictPort: true,
ws: { clientPort: 5173 },
}Without ws.clientPort the client opens ws://localhost:3000 instead. NestJS proxies that handshake through to Vite, so HMR still works — but the socket is torn down whenever Nest restarts (every edit under nest start --watch, a controller or service edit under nestjs-ssr dev). Connecting straight to Vite keeps the channel alive across restarts, so hot updates stay hot instead of degrading to full page reloads.
strictPort is worth setting too: without it Vite silently slides to the next free port and the NestJS proxy ends up pointing at nothing.
NODE_ENV
Development mode is opt-in: NODE_ENV must be exactly development, or you must pass RenderModule.forRoot({ environment: 'development' }). Unset, test and anything else run the production pipeline, which expects a built client bundle and hides error detail.
The init CLI puts NODE_ENV=development in the generated dev:nest script. If you see NODE_ENV is not set - running in PRODUCTION mode at startup, add it to yours.
The reason it works this way: development mode proxies project sources through the Vite dev server and renders stack traces into error pages. Defaulting to that on an unset variable meant a deployment that forgot NODE_ENV shipped both.
Dev proxy scope
The proxy that forwards /src/*, /@* and /node_modules/* to Vite only answers loopback clients. Vite binds to localhost; NestJS binds every interface, so serving remote peers would expose your project's sources — and Vite's /@fs/ endpoint — to the whole network.
Host, Origin, socket-peer, and forwarding headers are checked together. This prevents a DNS-rebinding page or a local reverse proxy from turning a loopback socket into implicit authorization.
If your browser really is on another machine (container, LAN device testing), allowlist its exact public hostname and origin:
RenderModule.forRoot({
vite: {
port: 5173,
allowedHosts: ['dev.example.test'],
allowedOrigins: ['http://dev.example.test:3000'],
},
});When using a reverse proxy, preserve the original Host and forwarding headers; the public hostname must be in allowedHosts. Prefer an SSH tunnel when you can, since the Vite proxy exposes source files by design.
Running Separately
If you prefer separate terminals:
# Terminal 1
pnpm dev:vite
# Terminal 2
pnpm dev:nestCustom Vite Port
// app.module.ts
RenderModule.forRoot({
vite: { port: 3001 },
});Update your vite.config.ts and dev:vite script to match.
Production
pnpm build
pnpm start:prodVite builds optimized bundles. Manifest tells NestJS which chunks to load.