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 buildand have adist/folder (and optionallydist/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:
- API first: If
dist/vitek-api.mjsexists, it loads the bundle and mounts the API handler at/api. Requests to/api/*are handled by the same logic as in development. - Static files: Serves files from
dist/(e.g.index.html,assets/*.js,assets/*.css). - SPA fallback: For GET requests that are not under
/apiand do not match a file, it servesdist/index.htmlso 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 --> ResponseUsage
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 startThe 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 startOptions
| Option | Default | Description |
|---|---|---|
--dir | dist | Directory to serve (relative to current directory) |
--port | 3000 | Port to listen on |
--host | 0.0.0.0 | Host to bind to |
--cors | off | Enable CORS (permissive * origin) for the API |
--trust-proxy | off | Trust 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 APIProduction 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(ordist/vitek.config.js). The file must be in the same directory you pass to--dir(defaultdist).
Exports from vitek.config.mjs:
| Export | Type / signature | Description |
|---|---|---|
beforeApiRequest | (ctx, next) => void or array of such functions | Run 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 }) => void | Called once before the server listens. See Lifecycle hooks. |
onServerShutdown | () => void | Promise<void> | Called on SIGTERM/SIGINT before the server closes. |
maxBodySize | number | Max 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:
| Property | Type | Description |
|---|---|---|
api | ApiClient | Internal client to call your REST API (e.g. ctx.api.fetch('/api/jobs/run')) |
sockets | SocketEmitter | Emit to WebSocket clients (e.g. ctx.sockets.emit('chat', data)) |
server | http.Server | The 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.
