Powerful IoC library built on-top of InversifyJS inspired by NestJS's DI.
npm install @adimm/x-injection- Table of Contents
- Overview
- Features
- Installation
- Quick Start
- OOP-Style Modules
- Basic OOP Module
- Advanced OOP Patterns
- When to Use OOP vs Functional
- Core Concepts
- ProviderModule
- AppModule
- Blueprints
- Provider Tokens
- Injection Scopes
- Singleton (Default)
- Transient
- Request
- Module System
- Import/Export Pattern
- Re-exporting Modules
- Dynamic Module Updates
- Global Modules
- Advanced Features
- Events
- Middlewares
- Testing
- Resources
- Contributing
- Credits
- License
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.
- Modular Architecture - NestJS-style import/export system for clean dependency boundaries
- Isolated Containers - Each module manages its own InversifyJS container
- Flexible Scopes - Singleton, Transient, and Request-scoped providers
- Lazy Loading - Blueprint pattern for deferred module instantiation
- Lifecycle Hooks - onReady, onReset, onDispose for module lifecycle management
- Events & Middlewares - Deep customization through event subscriptions and middleware chains
- Framework Agnostic - Works in Node.js and browser environments
- TypeScript First - Full type safety with decorator support
``bash`
npm install @adimm/x-injection reflect-metadata
TypeScript Configuration (tsconfig.json):
`json`
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
Import reflect-metadata at your application's entry point:
`ts`
import 'reflect-metadata';
`ts
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.
`ts
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:
`ts
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
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:
`ts
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:
`ts[${String(this.module.id)}] ${action}
class BaseModule extends ProviderModuleClass {
protected logAction(action: string): void {
const logger = this.module.get(LoggerService);
logger.log();
}
}
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:
- You need custom business logic methods on the module itself
- You prefer class-based architecture
- You want computed properties or getters for providers
- You need initialization logic or state management in the module
- You're building a complex module with multiple related operations
Use Functional-style (ProviderModule.create()) when:
- You only need dependency injection without custom logic
- You prefer functional composition
- You want simpler, more concise code
- You're creating straightforward provider containers
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.
`ts`
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 resources
The global root module, automatically available in every application. Global modules are auto-imported into AppModule.
`ts
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.
`ts
// 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:
- Deferred instantiation for better startup performance
- Reusable module templates across your application
- Scoped singletons per importing module
xInjection supports four types of provider tokens:
1. Class Token (simplest):
`ts
@Injectable()
class ApiService {}
providers: [ApiService];
`
2. Class Token with Substitution:
`ts`
providers: [{ provide: ApiService, useClass: MockApiService }];
3. Value Token (constants):
`ts`
providers: [{ provide: 'API_KEY', useValue: 'secret-key-123' }];
4. Factory Token (dynamic):
`ts`
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:
`ts
@Injectable() // Singleton by default
class DatabaseService {}
Module.get(DatabaseService) === Module.get(DatabaseService); // true
`
New instance on every resolution:
`ts
@Injectable(InjectionScope.Transient)
class RequestLogger {}
Module.get(RequestLogger) === Module.get(RequestLogger); // false
`
Single instance per resolution tree (useful for request-scoped data):
`ts
@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:
`ts
// 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:
`ts
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:
`ts
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):
`ts
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:
`ts
const LoggerModule = ProviderModule.create({
id: 'LoggerModule',
isGlobal: true,
providers: [LoggerService],
exports: [LoggerService],
});
// LoggerService now available in all modules without explicit import
`
> [!WARNING]
> These features provide deep customization but can add complexity. Use them only when necessary.
Subscribe to module lifecycle events for monitoring and debugging:
`ts
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 →
> [!WARNING]
> Always unsubscribe to prevent memory leaks. Events fire after middlewares.
Intercept and transform provider resolution before values are returned:
`ts
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:
`ts`
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 →
> [!CAUTION]
>
> - Returning false aborts the chain (no value returned)true
> - Returning passes value unchanged
> - Middlewares execute in registration order
> - Always handle errors in middleware chains
Create mock modules easily using blueprint cloning:
`ts
// 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