Overview

Run Vercube as a Vite plugin using the Environment API
Experimental. The Vite integration is in an early stage. APIs may change between releases.

@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

Requirements: Vite 8, and experimentalDecorators enabled in tsconfig.json.

tsconfig.json
{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": false
  }
}
Vercube's DI does not rely on emitDecoratorMetadata (it uses explicit @Inject(Token)), so you can leave it off.

Quick start

Add the plugin

vite.config.ts
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.

src/Controllers/UserController.ts
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

Auto-discovery uses @Injectable() as an opt-in signal. Decorate every service you want registered automatically.
src/Services/UserService.ts
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().

vite.config.ts
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

OptionTypeDefaultDescription
rootDirstringVite rootProject root used to resolve scanDirs and setupFile.
scanDirsstring[]['src']Directories scanned recursively for @Controller and @Injectable classes.
setupFilestringundefinedModule whose default export (app: App) => void | Promise<void> runs before the app initializes. See Setup file.
runnerstring'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.

vite.config.ts
vercube({ setupFile: './src/Boot/Setup.ts' })
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.

src/Boot/Setup.ts
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.

Previous

Overview

How Vercube’s default build relates to production and development, and where to deploy.

Next