VelaVela
Fundamentals

Dependency Injection

Vela's IoC container provides automatic constructor injection with support for scopes and custom tokens.

Vela includes a built-in IoC (Inversion of Control) container that handles dependency resolution automatically. When a class is registered as a provider, the container resolves its constructor dependencies and manages instance lifecycles.

Container API

The container exposes these core methods:

MethodDescription
register()Register a provider with the container
resolve()Synchronously resolve a provider by token
resolveAsync()Asynchronously resolve a provider (for async factories)
has()Check if a token is registered
createChild()Create a child container that inherits registrations

Constructor Injection

Constructor parameters are resolved automatically by type. The container inspects the constructor signature and provides matching instances:

@Injectable()
class SpiritsService {
  findAll() {
    return [{ name: 'Calcifer' }];
  }
}

@Injectable()
class SpiritsController {
  constructor(private readonly spiritsService: SpiritsService) {}

  getAll() {
    return this.spiritsService.findAll();
  }
}

The container sees that SpiritsController depends on SpiritsService, resolves it, and passes it to the constructor automatically.

Explicit Injection with @Inject

When the type alone is not enough to resolve a dependency — such as with interfaces, string tokens, or InjectionToken — use the @Inject() decorator:

import { Inject, Injectable, InjectionToken } from '@velajs/vela';

const API_KEY = new InjectionToken<string>('api-key');

@Injectable()
class ApiService {
  constructor(@Inject(API_KEY) private apiKey: string) {}
}

This is also necessary when multiple providers share the same type but are distinguished by token:

@Injectable()
class NotificationService {
  constructor(
    @Inject('EMAIL_SERVICE') private email: EmailService,
    @Inject('SMS_SERVICE') private sms: SmsService,
  ) {}
}

Scopes

Scopes control how many instances the container creates:

Singleton (default)

A single instance is shared across the entire application. This is the default behavior:

@Injectable() // Scope.SINGLETON by default
class ConfigService {
  private config = new Map<string, string>();

  get(key: string) {
    return this.config.get(key);
  }
}

Transient

A new instance is created every time the provider is resolved:

@Injectable({ scope: Scope.TRANSIENT })
class LoggerService {
  private id = crypto.randomUUID();

  log(message: string) {
    console.log(`[${this.id}] ${message}`);
  }
}

Each consumer that injects LoggerService receives its own instance.

Request

A unique instance is created for each incoming request:

@Injectable({ scope: Scope.REQUEST })
class RequestContext {
  public requestId = crypto.randomUUID();
  public startedAt = Date.now();
}

How Request Scope Works

For each incoming request, Vela creates a child container using createChild(). This child container:

  1. Inherits all singleton registrations from the parent
  2. Creates fresh instances for request-scoped providers
  3. Is discarded after the request completes

This means singleton providers are still shared across requests, while request-scoped providers get a clean instance per request.

InjectionToken

InjectionToken provides a way to create typed, non-class tokens for dependency injection. This is useful when the dependency is not a class (primitives, interfaces, configuration objects):

import { InjectionToken } from '@velajs/vela';

interface DatabaseConfig {
  host: string;
  port: number;
}

const DB_CONFIG = new InjectionToken<DatabaseConfig>('database-config');

Tokens can also include an optional factory default:

const TIMEOUT = new InjectionToken<number>('request-timeout', {
  factory: () => 5000,
});

Register and inject as a custom provider:

@Module({
  providers: [
    { provide: DB_CONFIG, useValue: { host: 'localhost', port: 5432 } },
  ],
})
class AppModule {}

@Injectable()
class DatabaseService {
  constructor(@Inject(DB_CONFIG) private config: DatabaseConfig) {}
}

Async Resolution

Factory providers can be asynchronous. Use resolveAsync() when the factory returns a promise:

@Module({
  providers: [
    {
      provide: 'DATABASE',
      useFactory: async (config: ConfigService) => {
        const db = new Database(config.get('DB_URL'));
        await db.connect();
        return db;
      },
      inject: [ConfigService],
    },
  ],
})
class AppModule {}

Async providers are resolved during application bootstrap before any requests are handled.

Edit on GitHub

Last updated on

On this page