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:
| Method | Description |
|---|---|
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:
- Inherits all singleton registrations from the parent
- Creates fresh instances for request-scoped providers
- 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.
Middleware
Middleware runs before route handlers, with access to the request context.
Guards
Guards determine whether a request should be handled by the route handler.
Last updated on