xInjection is a powerful Dependency Injection library built on InversifyJS, inspired by NestJS's modular architecture. It provides fine-grained control over dependency encapsulation through a module-based system where each module manages its own container with explicit import/export boundaries.
onReady, onReset, onDispose for module lifecycle managementnpm install @adimm/x-injection reflect-metadata
TypeScript Configuration (tsconfig.json):
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
Import reflect-metadata at your application's entry point:
import 'reflect-metadata';
import { Injectable, ProviderModule } from '@adimm/x-injection';
@Injectable()
class UserService {
getUser(id: string) {
return { id, name: 'John Doe' };
}
}
@Injectable()
class AuthService {
constructor(private userService: UserService) {}
login(userId: string) {
const user = this.userService.getUser(userId);
return `Logged in as ${user.name}`;
}
}
const AuthModule = ProviderModule.create({
id: 'AuthModule',
providers: [UserService, AuthService],
exports: [AuthService],
});
const authService = AuthModule.get(AuthService);
console.log(authService.login('123')); // "Logged in as John Doe"
For developers who prefer class-based architecture, xInjection provides ProviderModuleClass - a composition-based wrapper that prevents naming conflicts between your custom methods and the DI container methods.
import { Injectable, ProviderModuleClass } from '@adimm/x-injection';
@Injectable()
class UserService {
get(id: string) {
return { id, name: 'John Doe' };
}
}
@Injectable()
class AuthService {
constructor(private userService: UserService) {}
login(userId: string) {
const user = this.userService.get(userId);
return `Logged in as ${user.name}`;
}
}
// OOP-style module extending ProviderModuleClass
class AuthModule extends ProviderModuleClass {
constructor() {
super({
id: 'AuthModule',
providers: [UserService, AuthService],
exports: [AuthService],
});
}
authenticateUser(userId: string): string {
const authService = this.module.get(AuthService);
return authService.login(userId);
}
getUserById(userId: string) {
const userService = this.module.get(UserService);
return userService.get(userId);
}
}
// Instantiate and use
const authModule = new AuthModule();
console.log(authModule.authenticateUser('123')); // "Logged in as John Doe"
// All ProviderModule methods are available through the `.module` property
const authService = authModule.module.get(AuthService);
authModule.module.update.addProvider(NewService);
Module with Initialization Logic:
class DatabaseModule extends ProviderModuleClass {
private isConnected = false;
constructor() {
super({
id: 'DatabaseModule',
providers: [DatabaseService, ConnectionPool],
exports: [DatabaseService],
onReady: async (module) => {
console.log('DatabaseModule ready!');
},
});
}
async connect(): Promise<void> {
const dbService = this.module.get(DatabaseService);
await dbService.connect();
this.isConnected = true;
}
getConnectionStatus(): boolean {
return this.isConnected;
}
}
const dbModule = new DatabaseModule();
await dbModule.connect();
console.log(dbModule.getConnectionStatus()); // true
Module with Computed Properties:
class ApiModule extends ProviderModuleClass {
constructor() {
super({
id: 'ApiModule',
imports: [ConfigModule, LoggerModule],
providers: [ApiService, HttpClient],
exports: [ApiService],
});
}
// Computed property - lazy evaluation
get apiService(): ApiService {
return this.module.get(ApiService);
}
get httpClient(): HttpClient {
return this.module.get(HttpClient);
}
// Business logic using multiple services
async makeAuthenticatedRequest(url: string, token: string) {
const client = this.httpClient;
return client.request(url, {
headers: { Authorization: `Bearer ${token}` },
});
}
}
const apiModule = new ApiModule();
const response = await apiModule.makeAuthenticatedRequest('/users', 'token');
Module Composition:
class BaseModule extends ProviderModuleClass {
protected logAction(action: string): void {
const logger = this.module.get(LoggerService);
logger.log(`[${String(this.module.id)}] ${action}`);
}
}
class UserModule extends BaseModule {
constructor() {
super({
id: 'UserModule',
providers: [UserService, UserRepository],
exports: [UserService],
});
}
createUser(name: string) {
this.logAction(`Creating user: ${name}`);
const userService = this.module.get(UserService);
return userService.create(name);
}
deleteUser(id: string) {
this.logAction(`Deleting user: ${id}`);
const userService = this.module.get(UserService);
return userService.delete(id);
}
}
Use OOP-style (extends ProviderModuleClass) when:
Use Functional-style (ProviderModule.create()) when:
Key Point: Both styles are fully compatible and can be mixed within the same application. ProviderModuleClass uses composition (contains a ProviderModule as this.module), preventing method name conflicts while providing identical DI functionality.
The fundamental building block of xInjection. Similar to NestJS modules, each ProviderModule encapsulates related providers with explicit control over what's exposed.
const DatabaseModule = ProviderModule.create({
id: 'DatabaseModule',
imports: [ConfigModule], // Modules to import
providers: [DatabaseService], // Services to register
exports: [DatabaseService], // What to expose to importers
});
Key Methods:
Module.get(token) - Resolve a provider instanceModule.update.addProvider() - Dynamically add providersModule.update.addImport() - Import other modules at runtimeModule.dispose() - Clean up module resourcesThe global root module, automatically available in every application. Global modules are auto-imported into AppModule.
import { AppModule } from '@adimm/x-injection';
// Add global providers
AppModule.update.addProvider(LoggerService);
// Access from any module
const anyModule = ProviderModule.create({ id: 'AnyModule' });
const logger = anyModule.get(LoggerService);
Blueprints allow you to define module configurations without instantiating them, enabling lazy loading and template reuse.
// Define blueprint
const DatabaseModuleBp = ProviderModule.blueprint({
id: 'DatabaseModule',
providers: [DatabaseService],
exports: [DatabaseService],
});
// Import blueprint (auto-converts to module)
const AppModule = ProviderModule.create({
id: 'AppModule',
imports: [DatabaseModuleBp],
});
// Or create module from blueprint later
const DatabaseModule = ProviderModule.create(DatabaseModuleBp);
Benefits:
xInjection supports four types of provider tokens:
1. Class Token (simplest):
@Injectable()
class ApiService {}
providers: [ApiService];
2. Class Token with Substitution:
providers: [{ provide: ApiService, useClass: MockApiService }];
3. Value Token (constants):
providers: [{ provide: 'API_KEY', useValue: 'secret-key-123' }];
4. Factory Token (dynamic):
providers: [
{
provide: 'DATABASE_CONNECTION',
useFactory: (config: ConfigService) => createConnection(config.dbUrl),
inject: [ConfigService],
},
];
Control provider lifecycle with three scope types (priority order: token > decorator > module default):
Cached after first resolution - same instance every time:
@Injectable() // Singleton by default
class DatabaseService {}
Module.get(DatabaseService) === Module.get(DatabaseService); // true
New instance on every resolution:
@Injectable(InjectionScope.Transient)
class RequestLogger {}
Module.get(RequestLogger) === Module.get(RequestLogger); // false
Single instance per resolution tree (useful for request-scoped data):
@Injectable(InjectionScope.Request)
class RequestContext {}
@Injectable(InjectionScope.Transient)
class Controller {
constructor(
public ctx1: RequestContext,
public ctx2: RequestContext
) {}
}
const controller = Module.get(Controller);
controller.ctx1 === controller.ctx2; // true (same resolution)
const controller2 = Module.get(Controller);
controller.ctx1 === controller2.ctx1; // false (different resolution)
Setting Scopes:
// 1. In provider token (highest priority)
providers: [{ provide: Service, useClass: Service, scope: InjectionScope.Transient }];
// 2. In @Injectable decorator
@Injectable(InjectionScope.Request)
class Service {}
// 3. Module default (lowest priority)
ProviderModule.create({
id: 'MyModule',
defaultScope: InjectionScope.Transient,
});
Modules explicitly control dependency boundaries through imports and exports:
const DatabaseModule = ProviderModule.create({
id: 'DatabaseModule',
providers: [DatabaseService, InternalCacheService],
exports: [DatabaseService], // Only DatabaseService is accessible
});
const ApiModule = ProviderModule.create({
id: 'ApiModule',
imports: [DatabaseModule], // Gets access to DatabaseService
providers: [ApiService],
});
// ✅ Works
const dbService = ApiModule.get(DatabaseService);
// ❌ Error - InternalCacheService not exported
const cache = ApiModule.get(InternalCacheService);
Modules can re-export imported modules to create aggregation modules:
const CoreModule = ProviderModule.create({
id: 'CoreModule',
imports: [DatabaseModule, ConfigModule],
exports: [DatabaseModule, ConfigModule], // Re-export both
});
// Consumers get both DatabaseModule and ConfigModule
const AppModule = ProviderModule.create({
imports: [CoreModule],
});
Modules support runtime modifications (use sparingly for performance):
const module = ProviderModule.create({ id: 'DynamicModule' });
// Add providers dynamically
module.update.addProvider(NewService);
module.update.addProvider(AnotherService, true); // true = also export
// Add imports dynamically
module.update.addImport(DatabaseModule, true); // true = also export
Important: Dynamic imports propagate automatically - if ModuleA imports ModuleB, and ModuleB dynamically imports ModuleC (with export), ModuleA automatically gets access to ModuleC's exports.
Mark modules as global to auto-import into AppModule:
const LoggerModule = ProviderModule.create({
id: 'LoggerModule',
isGlobal: true,
providers: [LoggerService],
exports: [LoggerService],
});
// LoggerService now available in all modules without explicit import
These features provide deep customization but can add complexity. Use them only when necessary.
Subscribe to module lifecycle events for monitoring and debugging:
import { DefinitionEventType } from '@adimm/x-injection';
const module = ProviderModule.create({
id: 'MyModule',
providers: [MyService],
});
const unsubscribe = module.update.subscribe(({ type, change }) => {
if (type === DefinitionEventType.GetProvider) {
console.log('Provider resolved:', change);
}
if (type === DefinitionEventType.Import) {
console.log('Module imported:', change);
}
});
// Clean up when done
unsubscribe();
Available Events: GetProvider, Import, Export, AddProvider, RemoveProvider, ExportModule - Full list →
Always unsubscribe to prevent memory leaks. Events fire after middlewares.
Intercept and transform provider resolution before values are returned:
import { MiddlewareType } from '@adimm/x-injection';
const module = ProviderModule.create({
id: 'MyModule',
providers: [PaymentService],
});
// Transform resolved values
module.middlewares.add(MiddlewareType.BeforeGet, (provider, token, inject) => {
// Pass through if not interested
if (!(provider instanceof PaymentService)) return true;
// Use inject() to avoid infinite loops
const logger = inject(LoggerService);
logger.log('Payment service accessed');
// Transform the value
return {
timestamp: Date.now(),
value: provider,
};
});
const payment = module.get(PaymentService);
// { timestamp: 1234567890, value: PaymentService }
Control export access:
module.middlewares.add(MiddlewareType.OnExportAccess, (importerModule, exportToken) => {
// Restrict access based on importer
if (importerModule.id === 'UntrustedModule' && exportToken === SensitiveService) {
return false; // Deny access
}
return true; // Allow
});
Available Middlewares: BeforeGet, BeforeAddProvider, BeforeAddImport, OnExportAccess - Full list →
false aborts the chain (no value returned)true passes value unchangedCreate mock modules easily using blueprint cloning:
// Production module
const ApiModuleBp = ProviderModule.blueprint({
id: 'ApiModule',
providers: [UserService, ApiService],
exports: [ApiService],
});
// Test module - clone and override
const ApiModuleMock = ApiModuleBp.clone().updateDefinition({
id: 'ApiModuleMock',
providers: [
{ provide: UserService, useClass: MockUserService },
{
provide: ApiService,
useValue: {
sendRequest: jest.fn().mockResolvedValue({ data: 'test' }),
},
},
],
});
// Use in tests
const testModule = ProviderModule.create({
imports: [ApiModuleMock],
});
📚 Full API Documentation - Complete TypeDoc reference
⚛️ React Integration - Official React hooks and providers
💡 GitHub Issues - Bug reports and feature requests
Contributions welcome! Please ensure code follows the project style guidelines.
Author: Adi-Marian Mutu Built on: InversifyJS Logo: Alexandru Turica
MIT © Adi-Marian Mutu