Skip to content

Development ​

Starting Development ​

bash
pnpm start:dev

This runs both Vite and NestJS concurrently with full HMR support.

View edits without restarts ​

Projects created by init run Nest through nestjs-ssr dev:

json
"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 ​

  1. Vite dev server runs on port 5173 (client assets, HMR)
  2. NestJS server runs on port 3000 (SSR, API)
  3. NestJS proxies asset requests (/src/, /@, /node_modules/) and Vite's HMR WebSocket to Vite
  4. 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:

javascript
// 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:

typescript
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:

bash
# Terminal 1
pnpm dev:vite

# Terminal 2
pnpm dev:nest

Custom Vite Port ​

typescript
// app.module.ts
RenderModule.forRoot({
  vite: { port: 3001 },
});

Update your vite.config.ts and dev:vite script to match.

Production ​

bash
pnpm build
pnpm start:prod

Vite builds optimized bundles. Manifest tells NestJS which chunks to load.

Released under the MIT License.