Overview
The @vercube/nitro package lets you run Vercube controllers inside a Nitro application. You get Vercube's decorator-based routing and full dependency injection while keeping everything that makes Nitro powerful: multi-platform deployment, built-in storage, and file-based routing.
Installation
$ pnpm add @vercube/nitro
$ npm install @vercube/nitro
$ bun add @vercube/nitro
Requirements
- Nitro 3 (currently in beta) is required
experimentalDecorators: truemust be set in yourtsconfig.json- The
rolldownbundler must be used (Nitro validates this at build time)
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
Quick Start
Configure Nitro
Add the vercubeNitro module to your nitro.config.ts:
import { vercubeNitro } from '@vercube/nitro';
import { defineConfig } from 'nitro';
export default defineConfig({
modules: [
vercubeNitro({
scanDirs: ['routes', 'services'],
}),
],
serverDir: './src',
});
Create a Controller
Place a @Controller class anywhere inside your scanned directories:
import { Controller, Get, Post, Param } from '@vercube/core';
import { Inject } from '@vercube/di';
import { UserService } from '../services/UserService';
@Controller('/api/users')
export class UserController {
@Inject(UserService)
private userService: UserService;
@Get('/')
async list() {
return this.userService.findAll();
}
@Get('/:id')
async get(@Param('id') id: string) {
return this.userService.findById(id);
}
@Post('/')
async create() {
return this.userService.create();
}
}
Create a Service
@Injectable() - the DI container discovers and binds classes automatically. In the Nitro integration, @Injectable() is required on every service you want auto-registered. The plugin uses it as an explicit opt-in signal so that only the classes you actually need get bound to the container, saving memory and avoiding unnecessary bindings at startup.import { Injectable } from '@vercube/di';
@Injectable()
export class UserService {
findAll() {
return [{ id: '1', name: 'Alice' }];
}
findById(id: string) {
return { id, name: 'Alice' };
}
create() {
return { id: '2', name: 'Bob' };
}
}
Run
npx nitro dev
How It Works
The module scans your source directories at build time using AST parsing and generates a Nitro virtual plugin module that:
- Creates a Vercube application via
createNitroApp() - Binds all discovered
@Controllerand@Injectableclasses to the DI container - Excludes
BaseMiddlewaresubclasses from Nitro's native middleware handling - Registers a global route handler that delegates every request to the Vercube app
Nitro's own file-based routes continue to work normally alongside Vercube controllers.
Module Options
| Option | Type | Default | Description |
|---|---|---|---|
scanDirs | string[] | ['api', 'routes', 'services', 'repositories'] | Directories to scan for @Injectable services. Merged with any value you provide via defu. |
setupFile | string | undefined | Path to a file that exports a default function (app: App) => void | Promise<void>. Called after all auto-discovered bindings are registered but before flushQueue(). Use it to bind tokens that auto-discovery cannot handle. |
Controllers (@Controller) are always scanned from apiDir (default: api/) and routesDir (default: routes/) - these come from Nitro's own config and are not affected by scanDirs.
Services (@Injectable) are scanned exclusively from scanDirs. The default covers the most common layout. Add extra directories if your project places services elsewhere:
// Default - scans api/, routes/, services/, repositories/
vercubeNitro()
// Extended - add a custom directory on top of the defaults
vercubeNitro({
scanDirs: ['api', 'routes', 'services', 'repositories', 'handlers'],
})
Customizing the Container
There are two ways to add custom bindings or perform setup that auto-discovery cannot handle.
setupFile (recommended)
Pass a setupFile path pointing to a module that exports a default function. The module is bundled by Rolldown alongside the rest of your app and the function is called after all auto-discovered controllers and services are registered but before flushQueue() - so you can override or extend bindings freely.
import { vercubeNitro } from '@vercube/nitro';
import { defineConfig } from 'nitro';
export default defineConfig({
modules: [
vercubeNitro({
setupFile: './src/container.ts',
}),
],
});
import type { App } from '@vercube/core';
import { DatabaseToken } from './tokens';
import { PostgresDatabase } from './infra/PostgresDatabase';
export default async (app: App) => {
app.container.bind(DatabaseToken, PostgresDatabase);
};
useVercubeApp() (runtime access)
If you need access to the app instance after startup - for example inside a Nitro plugin or a request handler - use the useVercubeApp() helper:
import { useVercubeApp } from '@vercube/nitro';
const { container } = useVercubeApp();
container.bind($MyService, MyService);
useVercubeApp() after the Nitro app has fully initialized (e.g. inside a Nitro plugin or a request handler). Calling it too early will return undefined. For container setup, prefer setupFile instead.