A lightweight IoC container for managing service dependencies in your application.
The DI system provides:
- Constructor injection
- Property injection
- Singleton and scoped lifetimes
- Decorator-based registration
import { ContainerService, container } from '@relax.js/core/di';
@ContainerService()
class LoggerService {
log(message: string) {
console.log(`[LOG] ${message}`);
}
}
// Resolve and use
const logger = container.resolve(LoggerService);
logger.log('Hello!');Use the @ContainerService decorator to register services:
@ContainerService()
class ConfigService {
apiUrl = '/api/v1';
}
@ContainerService({ inject: [ConfigService] })
class ApiClient {
constructor(private config: ConfigService) {}
fetch(path: string) {
return fetch(this.config.apiUrl + path);
}
}For cases where you can't use decorators (e.g. registering an existing instance):
import { serviceCollection } from '@relax.js/core/di';
// Register with existing instance
const config = { apiUrl: '/api' };
serviceCollection.register(ConfigService, {
inject: [],
instance: config
});
// Register with custom key
serviceCollection.register(CacheService, {
key: 'primaryCache',
scope: 'global',
inject: []
});Same instance returned every time:
@ContainerService({ scope: 'global' })
class DatabaseConnection {
// ...
}
const db1 = container.resolve(DatabaseConnection);
const db2 = container.resolve(DatabaseConnection);
// db1 === db2New instance for each container scope:
@ContainerService({ scope: 'closest' })
class RequestContext {
// ...
}Dependencies are passed to the constructor in order:
@ContainerService({
inject: [LoggerService, ConfigService, DatabaseConnection]
})
class UserRepository {
constructor(
private logger: LoggerService,
private config: ConfigService,
private db: DatabaseConnection
) {}
}The @Inject decorator resolves a dependency and assigns it to a class field.
The service is resolved when the instance is created, so the field is available
in all methods including connectedCallback for web components.
import { ContainerService, Inject } from '@relax.js/core/di';
@ContainerService({ inject: [] })
class OrderService {
@Inject(LoggerService)
private logger!: LoggerService;
@Inject('primaryCache') // resolve by string key
private cache!: CacheService;
}Alternatively, declare property injection in the registration options:
@ContainerService({
inject: [],
properties: {
logger: LoggerService,
cache: 'primaryCache' // string key
}
})
class OrderService {
logger!: LoggerService;
cache!: CacheService;
}Web components use @Inject to access services. Do not use @ContainerService on
components since the browser creates them with zero constructor arguments.
import { Inject } from '@relax.js/core/di';
class UserPanel extends HTMLElement {
@Inject(UserService)
private userService!: UserService;
@Inject(LoggerService)
private logger!: LoggerService;
connectedCallback() {
// Injected fields are resolved and ready to use
const user = this.userService.getCurrentUser();
this.logger.log('UserPanel connected');
this.render(user);
}
}
customElements.define('user-panel', UserPanel);This works the same way regardless of how the component is created:
// Created by application code
document.createElement('user-panel');
// Created by the browser from HTML
// <user-panel></user-panel>Register all services before defining custom elements. Services
decorated with @ContainerService register themselves when imported,
so import those modules first.
// main.ts - application entry point
// 1. Import services (registers them via @ContainerService)
import './services/ApiClient';
import './services/UserService';
// 2. Register any manual services
serviceCollection.register(ConfigService, {
inject: [],
instance: { apiUrl: '/api/v1' },
});
// 3. Define custom elements (components can now resolve services)
import { UserPanel } from './components/UserPanel';
customElements.define('user-panel', UserPanel);// By class
const service = container.resolve(MyService);
// By string key
const cache = container.resolve<CacheService>('primaryCache');All DI errors go through the global onError handler, so you can log, display, or suppress them.
Thrown when resolve() cannot find a registered service.
| Field | Description |
|---|---|
service |
The class name or string key that was requested |
registeredTypes |
Array of all registered class names |
registeredKeys |
Array of all registered string keys |
RelaxError: Failed to resolve service 'MyService'
{ service: 'MyService', registeredTypes: ['LoggerService', 'ConfigService'], registeredKeys: ['primaryCache'] }
Thrown during register() or registerByType() to catch configuration mistakes early.
| Error | Context | Cause |
|---|---|---|
| Service name collision | { service } |
Two different classes registered with the same class name |
| Service key already registered | { key, existingClass, newClass } |
A string key is reused for a different class |
| Instance and inject conflict | { service } |
Both instance and non-empty inject provided (inject is ignored) |
import { onError } from '@relax.js/core/utils';
onError((error, ctx) => {
if (error.context.service === 'OptionalPlugin') {
ctx.suppress();
return;
}
console.error(error.message, error.context);
});- Register early: Register all services at application startup
- Prefer constructor injection: More explicit than property injection
- Use global scope for stateless services: Like loggers, config
- Use closest scope for request-specific data: Like user context
- Avoid circular dependencies: Restructure to use events or mediators