Skip to content

Production server (vitek-serve)

After vite build, use vitek-serve to run your app (static assets + API) in production or locally in a production-like way.

When to use

  • You have run vite build and have a dist/ folder (and optionally dist/vitek-api.mjs).
  • You want one process that serves both the frontend and the API at /api/*.
  • You are deploying to a Node host or running locally to test the production build.

How it works

vitek-serve is a small Node server that:

  1. API first: If dist/vitek-api.mjs exists, it loads the bundle and mounts the API handler at /api. Requests to /api/* are handled by the same logic as in development.
  2. Static files: Serves files from dist/ (e.g. index.html, assets/*.js, assets/*.css).
  3. SPA fallback: For GET requests that are not under /api and do not match a file, it serves dist/index.html so client-side routing works.

Request flow:

mermaid
flowchart LR
  Request[HTTP Request] --> ApiCheck{Path starts with /api?}
  ApiCheck -->|Yes| ApiHandler[API handler from vitek-api.mjs]
  ApiCheck -->|No| Static[serve-static dist/]
  Static -->|File found| SendFile[Send file]
  Static -->|Not found GET| SPA[Send index.html]
  ApiHandler --> Response[Response]
  SendFile --> Response
  SPA --> Response

Usage

From your project root (where dist/ exists), run the vitek-serve binary (provided by the installed vitek-plugin). Add a script to your package.json:

json
{
  "scripts": {
    "start": "vitek-serve"
  }
}

Then run:

bash
pnpm start
# or
npm start

The server listens at http://localhost:3000 by default.

Environment variables

If you do not pass --port or --host, vitek-serve uses process.env.PORT and process.env.HOST when set (e.g. on Heroku, Railway). CLI flags override env.

bash
PORT=8080 HOST=0.0.0.0 pnpm start

Options

OptionDefaultDescription
--dirdistDirectory to serve (relative to current directory)
--port3000Port to listen on
--host0.0.0.0Host to bind to
--corsoffEnable CORS (permissive * origin) for the API
--trust-proxyoffTrust X-Forwarded-* headers (use when behind nginx, Caddy, or similar)

Examples:

bash
vitek-serve --port 8080
vitek-serve --dir=dist --port 3000 --host 127.0.0.1
vitek-serve --trust-proxy   # when behind a reverse proxy (correct client IP and URL)
vitek-serve --cors          # enable CORS for the API

Production config (vitek.config.mjs)

To run beforeApiRequest or onError hooks in production, add a config file that vitek-serve will load from the output directory:

  • Path: dist/vitek.config.mjs (or dist/vitek.config.js). The file must be in the same directory you pass to --dir (default dist).

Exports from vitek.config.mjs:

ExportType / signatureDescription
beforeApiRequest(ctx, next) => void or array of such functionsRun before each API request (e.g. auth, logging). Same as plugin option.
onError(err, req, res) => void | Promise<void>Custom error handler when a non-HttpError is thrown. Same as plugin option.
onServerStart(ctx: { api, sockets?, server }) => voidCalled once before the server listens. See Lifecycle hooks.
onServerShutdown() => void | Promise<void>Called on SIGTERM/SIGINT before the server closes.
maxBodySizenumberMax request body size in bytes (413 when exceeded). See Security.

Example vitek.config.mjs in your project root, and ensure it is copied to dist/ during build (e.g. via Vite’s publicDir or a copy step):

javascript
// dist/vitek.config.mjs (or build so this file ends up in dist/)
export function beforeApiRequest(ctx, next) {
  // e.g. auth, logging
  next();
}

export function onError(err, req, res) {
  res.statusCode = 503;
  res.setHeader('Content-Type', 'application/json');
  res.end(JSON.stringify({ error: 'Service Unavailable' }));
}

If the file is missing or fails to load, vitek-serve continues without these hooks and logs a warning.

Lifecycle hooks (onServerStart / onServerShutdown)

You can run logic when the server starts or when it is shutting down (e.g. SIGTERM, SIGINT) by exporting onServerStart and optionally onServerShutdown from dist/vitek.config.mjs.

onServerStart(ctx)

Called once before the server starts listening. Use it to start timers, cron jobs, or other in-process scheduled work. The server is not accepting connections yet; use setInterval or node-cron so that when the callback runs, the server is already up (and ctx.api.fetch() will work if you call your API from the callback).

Context:

PropertyTypeDescription
apiApiClientInternal client to call your REST API (e.g. ctx.api.fetch('/api/jobs/run'))
socketsSocketEmitterEmit to WebSocket clients (e.g. ctx.sockets.emit('chat', data))
serverhttp.ServerThe HTTP server instance (for cleanup or low-level use)

Example – cron in-process with setInterval:

javascript
// dist/vitek.config.mjs
export function onServerStart({ api, server }) {
  const interval = setInterval(async () => {
    await api.fetch('/api/jobs/cleanup', { method: 'POST' });
  }, 60_000);
  server.on('close', () => clearInterval(interval));
}

Example – with node-cron (optional dependency):

javascript
// dist/vitek.config.mjs
import cron from 'node-cron';

export function onServerStart({ api }) {
  cron.schedule('0 * * * *', () => api.fetch('/api/jobs/hourly', { method: 'POST' }));
}

onServerShutdown()

Called when the process receives SIGTERM or SIGINT (e.g. Ctrl+C, container stop). Use it to clear timers, close database connections, or flush buffers. After the hook runs (or if it throws), vitek-serve closes the server and exits.

javascript
// dist/vitek.config.mjs
let intervalId;

export function onServerStart() {
  intervalId = setInterval(() => {}, 5000);
}

export function onServerShutdown() {
  if (intervalId) clearInterval(intervalId);
}

When the API is not available

If dist/vitek-api.mjs is missing (e.g. you set buildApi: false or have no API routes), vitek-serve still starts and serves only static files and the SPA fallback. You will see a one-line log: [vitek-serve] No API bundle found; serving static files only.

Relation to vite preview

vite preview is Vite's built-in way to preview the static build locally. It is not the recommended way to serve the app with the API in production. For production (or local production-like runs with the API), use vitek-serve.

Next steps

To run vitek-serve behind a reverse proxy (nginx, Caddy), with a process manager (PM2), or in Docker, see Deployment & integrations.