Overview
@vercube/vite runs a Vercube server as a Vite plugin, built on Vite's Environment API. You write decorator based controllers as usual and get Vite's dev server, fast server side HMR, and a single build command. Discovery is zero config: controllers and services are found by scanning your source.
It is a standalone alternative to the Nitro integration and does not touch the vercube dev / vercube build CLI. You drive it with vite and vite build.
A common setup is a frontend (Vue, React, plain JS) served by Vite next to a Vercube API on the same server. Vercube handles only the routes you define, everything else stays with Vite. See Using with a frontend.
Installation
$ pnpm add -D @vercube/vite vite
$ npm install -D @vercube/vite vite
$ bun add -D @vercube/vite vite
Requirements: Vite 8, and experimentalDecorators enabled in tsconfig.json.
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": false
}
}
emitDecoratorMetadata (it uses explicit @Inject(Token)), so you can leave it off.Quick start
Add the plugin
import { vercube } from '@vercube/vite';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [vercube()],
server: { port: 3000 },
});
Write a controller
Place a @Controller anywhere under src/. It is discovered automatically, with no manual registration.
import { Controller, Get, Param } from '@vercube/core';
import { Inject } from '@vercube/di';
import { UserService } from '../Services/UserService';
@Controller('/api/users')
export default class UserController {
@Inject(UserService)
private userService: UserService;
@Get('/')
list() {
return this.userService.findAll();
}
@Get('/:id')
get(@Param('id') id: string) {
return this.userService.findById(id);
}
}
Write a service
@Injectable() as an opt-in signal. Decorate every service you want registered automatically.import { Injectable } from '@vercube/di';
@Injectable()
export class UserService {
findAll() {
return [{ id: '1', name: 'Alice' }];
}
findById(id: string) {
return { id, name: 'Alice' };
}
}
Run
npx vite
Add or edit a controller while the server runs and the change is picked up live, no restart needed.
How it works
The plugin defines a dedicated Vite environment named vercube. At startup it scans your source (@vercube/scan) for @Controller and @Injectable classes and generates a server entry that creates the app, binds the discovered classes, flushes the container (which registers routes through decorator initialization), and exports a fetch handler.
In dev that entry runs inside an isolated env-runner worker through Vite's ModuleRunner. Vercube claims only the routes you define: a request matching a discovered route is forwarded into the worker, everything else falls through to Vite. Editing or adding a controller reloads the worker.
The generated entry lives at node_modules/.vercube/server-entry.mjs, so it stays out of source control.
Using with a frontend
Because Vercube claims only its own routes, the plugin sits next to a frontend that Vite serves on the same server. Add your framework's plugin alongside vercube().
import { vercube } from '@vercube/vite';
import vue from '@vitejs/plugin-vue';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [
vue(),
vercube({ scanDirs: ['src/server'], setupFile: './src/server/Boot/Setup.ts' }),
],
});
Vite serves index.html, your components and HMR. Requests to your @Controller routes (for example /api/*) go to Vercube. A full Vite + Vue example lives in examples/vite.
Build and run
npx vite build
node dist/index.mjs
vite build builds the frontend (when there is one) into dist/public, then bundles the server into dist/index.mjs. Dependencies are kept external, so the bundle stays small and imports them from node_modules at runtime.
dist/
index.mjs # the Vercube server
public/ # the frontend, only if the project has one
index.html
assets/…
node dist/index.mjs serves your API routes and the static frontend from dist/public (with index.html for /) from a single process, the same way the dev server does. The entry also exports fetch, so you can mount it in any Web fetch environment (edge runtimes, serverless adapters, tests) instead of starting the listener.
Options
| Option | Type | Default | Description |
|---|---|---|---|
rootDir | string | Vite root | Project root used to resolve scanDirs and setupFile. |
scanDirs | string[] | ['src'] | Directories scanned recursively for @Controller and @Injectable classes. |
setupFile | string | undefined | Module whose default export (app: App) => void | Promise<void> runs before the app initializes. See Setup file. |
runner | string | 'node-worker' | The env-runner runner used to execute server code in dev. |
Setup file
Auto-discovery binds @Controller and @Injectable classes. Anything it cannot infer, such as registering plugins, mounting storage, or binding an interface token to an implementation, goes in a setupFile. It runs as createApp's setup hook, before the app initializes, which is early enough to register plugins.
vercube({ setupFile: './src/Boot/Setup.ts' })
import type { App } from '@vercube/core';
import { AuthProvider } from '@vercube/auth';
import { StorageManager } from '@vercube/storage';
import { MemoryStorage } from '@vercube/storage/drivers/MemoryStorage';
import { BasicAuthenticationProvider } from '../Services/BasicAuthenticationProvider';
export default async function setup(app: App) {
app.container.bind(AuthProvider, BasicAuthenticationProvider);
app.container.bind(StorageManager);
app.container.get(StorageManager).mount({ storage: MemoryStorage });
}
WebSockets
The @vercube/ws plugin works in both dev and production. Register it from your setupFile and define WebSocket controllers (@Controller + @Namespace) as usual. They are auto-discovered alongside HTTP controllers.
import { WebsocketPlugin } from '@vercube/ws';
import type { App } from '@vercube/core';
export default async function setup(app: App) {
app.addPlugin(WebsocketPlugin);
}
The dev server forwards upgrade handshakes into the worker. The production server handles them natively through srvx.