Overview

Use Vercube controllers and DI inside a Nitro application - experimental
Experimental. The Nitro integration is in an early experimental stage. APIs may change without notice between releases. Use in production at your own risk.

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

Requirements

  • Nitro 3 (currently in beta) is required
  • experimentalDecorators: true must be set in your tsconfig.json
  • The rolldown bundler must be used (Nitro validates this at build time)
Nitro 3 beta. This integration targets Nitro 3, which is itself currently in beta. Both the Nitro API and this package may introduce breaking changes before their stable releases.
tsconfig.json
{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  }
}

Quick Start

Configure Nitro

Add the vercubeNitro module to your nitro.config.ts:

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:

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

Nitro only. In a standard Vercube application you do not need @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.
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' };
  }

  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:

  1. Creates a Vercube application via createNitroApp()
  2. Binds all discovered @Controller and @Injectable classes to the DI container
  3. Excludes BaseMiddleware subclasses from Nitro's native middleware handling
  4. 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

OptionTypeDefaultDescription
scanDirsstring[]['api', 'routes', 'services', 'repositories']Directories to scan for @Injectable services. Merged with any value you provide via defu.
setupFilestringundefinedPath 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:

nitro.config.ts
// 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.

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.

nitro.config.ts
import { vercubeNitro } from '@vercube/nitro';
import { defineConfig } from 'nitro';

export default defineConfig({
  modules: [
    vercubeNitro({
      setupFile: './src/container.ts',
    }),
  ],
});
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);
Only call 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.
Previous

Storage

Using Vercube StorageManager backed by Nitro's built-in storage

Next