@adimm/x-injection
v3.0.4
Published
Powerful IoC library built on-top of InversifyJS inspired by NestJS's DI.
Maintainers
Readme
A powerful, modular dependency injection library for TypeScript — Built on InversifyJS, inspired by NestJS's elegant module architecture.
TL;DR — Mark classes with
@Injectable(), group them intoProviderModules with explicitimports/exports, and callmodule.get(MyService). Dependencies are wired automatically, scoped correctly, and fully testable without touching production code.
Table of Contents
- Table of Contents
- What Problems Does This Solve?
- Installation
- Quick Start
- Core Concepts
- Provider Tokens
- Injection Scopes
- Module System
- Dependency Injection
- Lifecycle Hooks
- Events System
- Middlewares
- Testing
- OOP-Style Modules with ProviderModuleClass
- Advanced Module API
- Hierarchical Dependency Injection
- Resources
- Contributing
- Credits
- License
What Problems Does This Solve?
Modern applications face several dependency management challenges. Let's examine these problems and how xInjection solves them.
Problem 1: Manual Dependency Wiring
Without xInjection:
// Manually creating and wiring dependencies
class DatabaseService {
constructor(private readonly config: ConfigService) {}
}
class UserRepository {
constructor(private readonly db: DatabaseService) {}
}
class AuthService {
constructor(private readonly userRepo: UserRepository) {}
}
// Manual instantiation nightmare
const config = new ConfigService();
const database = new DatabaseService(config);
const userRepo = new UserRepository(database);
const authService = new AuthService(userRepo);
// Every file needs to repeat this setup
// Changes to constructors require updating all instantiation sitesWith xInjection:
@Injectable()
class DatabaseService {
constructor(private readonly config: ConfigService) {}
}
@Injectable()
class UserRepository {
constructor(private readonly db: DatabaseService) {}
}
@Injectable()
class AuthService {
constructor(private readonly userRepo: UserRepository) {}
}
const AuthModule = ProviderModule.create({
id: 'AuthModule',
providers: [ConfigService, DatabaseService, UserRepository, AuthService],
});
// Automatic dependency resolution
const authService = AuthModule.get(AuthService);
// All dependencies automatically injected!Problem 2: Tight Coupling and Testing Difficulty
Without xInjection:
class PaymentService {
// Hardcoded dependency - impossible to mock
private stripe = new StripeClient('api-key');
async charge(amount: number) {
return this.stripe.charge(amount);
}
}
// Testing requires hitting the real Stripe API
// No way to inject a mock without changing production codeWith xInjection:
@Injectable()
class PaymentService {
constructor(private readonly paymentGateway: PaymentGateway) {}
async charge(amount: number) {
return this.paymentGateway.charge(amount);
}
}
// Production: Use real Stripe
const ProductionModule = ProviderModule.create({
providers: [{ provide: PaymentGateway, useClass: StripePaymentGateway }, PaymentService],
});
// Testing: Use mock (no production code changes needed!)
const TestModule = ProviderModule.create({
providers: [{ provide: PaymentGateway, useClass: MockPaymentGateway }, PaymentService],
});
const testService = TestModule.get(PaymentService);
// testService.charge() → logs "Mock charge: $100" instead of hitting StripeProblem 3: Lack of Encapsulation
Without xInjection:
// Internal implementation details exposed
class CacheService {
// Should be private but other modules need access
public internalCache = new Map();
}
class DatabaseModule {
// Everything is public - no control over what gets used
public connection = createConnection();
public cache = new CacheService();
public queryBuilder = new QueryBuilder();
}
// Other modules can access internals they shouldn't
const cache = databaseModule.internalCache; // Bad!With xInjection:
const DatabaseModule = ProviderModule.create({
id: 'DatabaseModule',
providers: [ConnectionPool, CacheService, QueryBuilder],
exports: [QueryBuilder], // Only expose public API
});
// Other modules can only access QueryBuilder
// ConnectionPool and CacheService remain internal
const ApiModule = ProviderModule.create({
imports: [DatabaseModule],
});
// ✅ Works - QueryBuilder is exported
const queryBuilder = ApiModule.get(QueryBuilder);
// ❌ Error - CacheService not exported (properly encapsulated!)
const cache = ApiModule.get(CacheService); // throws InjectionProviderModuleMissingProviderErrorProblem 4: Lifecycle Management Complexity
Without xInjection:
class AppServices {
database: DatabaseService;
cache: CacheService;
async initialize() {
this.database = new DatabaseService();
await this.database.connect();
this.cache = new CacheService();
await this.cache.initialize();
// Manually track initialization order and cleanup
}
async cleanup() {
// Must remember to clean up in reverse order
await this.cache.dispose();
await this.database.disconnect();
}
}
// Easy to forget cleanup, leading to resource leaksWith xInjection:
const AppModule = ProviderModule.create({
id: 'AppModule',
providers: [DatabaseService, CacheService],
onReady: async (module) => {
// Initialization logic - called immediately after module creation
const db = module.get(DatabaseService);
await db.connect();
},
onDispose: () => {
return {
before: async (mod) => {
// Automatic cleanup in proper order
const db = mod.get(DatabaseService);
await db.disconnect();
},
};
},
});
// Lifecycle automatically managed — onDispose runs db.disconnect() automatically
await AppModule.dispose(); // ✅ No forgotten cleanup, no resource leaksxInjection is a Dependency Injection library built on InversifyJS, inspired by NestJS's modular architecture. It solves the pain points above through:
- 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,onDisposefor module lifecycle management - Events & Middlewares - Deep customization through event subscriptions and middleware chains
- OOP Support -
ProviderModuleClassfor class-based module architecture - Framework Agnostic - Works in Node.js and browser environments
- TypeScript First - Full type safety with decorator support
Installation
npm install @adimm/x-injection reflect-metadataTypeScript Configuration (tsconfig.json):
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}Import reflect-metadata at your application's entry point:
import 'reflect-metadata';Quick Start
import { Injectable, ProviderModule } from '@adimm/x-injection';
@Injectable()
class UserService {
getUser(id: string) {
return { id, name: 'John Doe' };
}
}
@Injectable()
class AuthService {
constructor(private readonly 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"Core Concepts
Services with @Injectable
The @Injectable() decorator marks a class as available for dependency injection. It enables automatic constructor parameter resolution.
import { Injectable, InjectionScope } from '@adimm/x-injection';
// Basic injectable service (Singleton by default)
@Injectable()
class LoggerService {
log(message: string) {
console.log(`[LOG] ${message}`);
}
}
// Injectable with scope specification
@Injectable(InjectionScope.Request)
class RequestContext {
requestId = Math.random();
}
// Complex service with dependencies
@Injectable()
class ApiService {
constructor(
private readonly logger: LoggerService,
private readonly context: RequestContext
) {}
async fetchData() {
this.logger.log(`Fetching data for request ${this.context.requestId}`);
return { data: 'example' };
}
}[!IMPORTANT] The
@Injectable()decorator is required for any class that:
- Has constructor dependencies
- Needs to be resolved from a module container
- Should be managed by the dependency injection system
Modules
Modules are the fundamental building blocks of xInjection. Each module encapsulates providers with explicit control over imports and exports.
import { ProviderModule } from '@adimm/x-injection';
// Define services
@Injectable()
class DatabaseService {
query(sql: string) {
return [{ id: 1, name: 'Result' }];
}
}
@Injectable()
class InternalCacheService {
// Internal-only service
private cache = new Map();
}
@Injectable()
class UserRepository {
constructor(
private readonly db: DatabaseService,
private readonly cache: InternalCacheService
) {}
findById(id: string) {
return this.db.query(`SELECT * FROM users WHERE id = ${id}`);
}
}
// Create module with encapsulation
const DatabaseModule = ProviderModule.create({
id: 'DatabaseModule',
providers: [DatabaseService, InternalCacheService, UserRepository],
exports: [UserRepository], // Only UserRepository accessible to importers
});
// Use the module
const ApiModule = ProviderModule.create({
id: 'ApiModule',
imports: [DatabaseModule],
});
// ✅ Works - UserRepository is exported
const userRepo = ApiModule.get(UserRepository);
// ❌ Throws error - InternalCacheService not exported
// const cache = ApiModule.get(InternalCacheService);Blueprints
Blueprints allow you to define module configurations without instantiating them, enabling lazy loading and template reuse.
import { ProviderModule } from '@adimm/x-injection';
@Injectable()
class ConfigService {
getConfig() {
return { apiUrl: 'https://api.example.com' };
}
}
// Define blueprint (not instantiated yet)
const ConfigModuleBp = ProviderModule.blueprint({
id: 'ConfigModule',
providers: [ConfigService],
exports: [ConfigService],
});
// Use blueprint in imports (auto-converts to module)
const ApiModule = ProviderModule.create({
id: 'ApiModule',
imports: [ConfigModuleBp], // Instantiated automatically when needed
});
// Or create module from blueprint explicitly
const ConfigModule = ProviderModule.create(ConfigModuleBp);
// Clone blueprints for testing
const ConfigModuleMock = ConfigModuleBp.clone().updateDefinition({
id: 'ConfigModuleMock',
providers: [{ provide: ConfigService, useValue: { getConfig: () => ({ apiUrl: 'mock' }) } }],
});Benefits of Blueprints:
- Deferred Instantiation - Only create modules when needed
- Reusable Templates - Define once, use in multiple places
- Testing - Clone and modify for test scenarios
- Scoped Singletons - Each importing module gets its own independent module instance converted from the blueprint
[!TIP] Use blueprints when you need the same module configuration in multiple places, or when you want to delay module creation until runtime.
[!IMPORTANT] When a blueprint is imported into multiple modules, each importing module receives its own separate instance of that blueprint — converted to a full module independently. This means that providers declared as
Singletoninside a blueprint are only singletons relative to the module that imported them, not globally. IfModuleAandModuleBboth importConfigModuleBp, they each get their ownConfigServicesingleton — the two instances are completely independent of each other.
AppModule
AppModule is the built-in global root container. Any module or blueprint created with isGlobal: true is automatically imported into it, making its exported providers available to every other module without an explicit imports declaration.
[!WARNING]
id: 'AppModule'is reserved — you cannot create your own module with that name- You cannot import
AppModuleinto other modulesSee Global Modules for full usage and best practices.
Provider Tokens
xInjection supports four types of provider tokens, each serving different use cases.
1. Class Token
The simplest form - just provide the class directly.
@Injectable()
class UserService {
getUsers() {
return [{ id: '1', name: 'Alice' }];
}
}
const MyModule = ProviderModule.create({
id: 'MyModule',
providers: [UserService], // Class token
});
const userService = MyModule.get(UserService);2. Class Token with Substitution
Use one class as the token but instantiate a different class. Perfect for polymorphism and testing.
@Injectable()
abstract class PaymentGateway {
abstract charge(amount: number): Promise<void>;
}
@Injectable()
class StripePaymentGateway extends PaymentGateway {
async charge(amount: number) {
console.log(`Charging $${amount} via Stripe`);
}
}
@Injectable()
class MockPaymentGateway extends PaymentGateway {
async charge(amount: number) {
console.log(`Mock charge: $${amount}`);
}
}
// Production
const ProductionModule = ProviderModule.create({
id: 'ProductionModule',
providers: [{ provide: PaymentGateway, useClass: StripePaymentGateway }],
});
// Testing
const TestModule = ProviderModule.create({
id: 'TestModule',
providers: [{ provide: PaymentGateway, useClass: MockPaymentGateway }],
});
const prodGateway = ProductionModule.get(PaymentGateway); // StripePaymentGateway
const testGateway = TestModule.get(PaymentGateway); // MockPaymentGateway3. Value Token
Provide constant values or pre-instantiated objects.
// Configuration values
const ConfigModule = ProviderModule.create({
id: 'ConfigModule',
providers: [
{ provide: 'API_KEY', useValue: 'secret-key-123' },
{ provide: 'API_URL', useValue: 'https://api.example.com' },
{ provide: 'MAX_RETRIES', useValue: 3 },
],
exports: ['API_KEY', 'API_URL', 'MAX_RETRIES'],
});
const apiKey = ConfigModule.get('API_KEY'); // 'secret-key-123'
const apiUrl = ConfigModule.get('API_URL'); // 'https://api.example.com'
const maxRetries = ConfigModule.get('MAX_RETRIES'); // 3
// Pre-instantiated objects
const existingLogger = new Logger();
const LoggerModule = ProviderModule.create({
id: 'LoggerModule',
providers: [{ provide: Logger, useValue: existingLogger }],
});4. Factory Token
Use a factory function to create providers dynamically. The inject parameter specifies dependencies.
@Injectable()
class ConfigService {
dbUrl = 'postgres://localhost:5432/mydb';
dbPort = 5432;
}
interface DatabaseConnection {
url: string;
port: number;
connected: boolean;
}
const DatabaseModule = ProviderModule.create({
id: 'DatabaseModule',
providers: [
ConfigService,
{
provide: 'DATABASE_CONNECTION',
useFactory: (config: ConfigService) => {
// Factory receives injected dependencies
return {
url: config.dbUrl,
port: config.dbPort,
connected: true,
};
},
inject: [ConfigService], // Dependencies to inject into factory
},
],
exports: ['DATABASE_CONNECTION'],
});
const connection = DatabaseModule.get<DatabaseConnection>('DATABASE_CONNECTION');
console.log(connection.url); // 'postgres://localhost:5432/mydb'[!TIP] Use factory tokens when:
- Provider creation requires complex logic
- You need to inject dependencies into the factory
- You're creating providers that depend on runtime configuration
- You need to create multiple instances with different configurations
Injection Scopes
Control provider lifecycle with three scope types. Scope priority order: token scope > decorator scope > module default scope.
Singleton (Default)
Cached after first resolution - same instance returned every time.
@Injectable() // Singleton by default
class DatabaseService {
connectionId = Math.random();
}
const MyModule = ProviderModule.create({
id: 'MyModule',
providers: [DatabaseService],
});
const db1 = MyModule.get(DatabaseService);
const db2 = MyModule.get(DatabaseService);
console.log(db1 === db2); // true
console.log(db1.connectionId === db2.connectionId); // trueTransient
New instance created on every resolution.
@Injectable(InjectionScope.Transient)
class RequestLogger {
requestId = Math.random();
}
const MyModule = ProviderModule.create({
id: 'MyModule',
providers: [RequestLogger],
});
const logger1 = MyModule.get(RequestLogger);
const logger2 = MyModule.get(RequestLogger);
console.log(logger1 === logger2); // false
console.log(logger1.requestId === logger2.requestId); // falseRequest
Single instance per resolution tree. All dependencies resolved in the same get() call share the same instance.
@Injectable(InjectionScope.Request)
class RequestContext {
requestId = Math.random();
}
@Injectable(InjectionScope.Transient)
class ServiceA {
constructor(public ctx: RequestContext) {}
}
@Injectable(InjectionScope.Transient)
class ServiceB {
constructor(public ctx: RequestContext) {}
}
@Injectable(InjectionScope.Transient)
class Controller {
constructor(
public serviceA: ServiceA,
public serviceB: ServiceB
) {}
}
const MyModule = ProviderModule.create({
id: 'MyModule',
providers: [RequestContext, ServiceA, ServiceB, Controller],
});
// First resolution tree
const controller1 = MyModule.get(Controller);
console.log(controller1.serviceA.ctx === controller1.serviceB.ctx); // true
// ServiceA and ServiceB share the same RequestContext
// Second resolution tree
const controller2 = MyModule.get(Controller);
console.log(controller2.serviceA.ctx === controller2.serviceB.ctx); // true
// New resolution, both services get a new shared RequestContext
// Different resolution trees get different contexts
console.log(controller1.serviceA.ctx === controller2.serviceA.ctx); // falseVisual Representation:
First module.get(Controller):
Controller (new) ──┬──> ServiceA (new) ──┐
│ ├──> RequestContext (SAME instance)
└──> ServiceB (new) ──┘
Second module.get(Controller):
Controller (new) ──┬──> ServiceA (new) ──┐
│ ├──> RequestContext (NEW instance)
└──> ServiceB (new) ──┘Scope Priority Order
Scopes are resolved in the following priority order (highest to lowest):
- Token scope (highest priority)
- Decorator scope
- Module default scope (lowest priority)
@Injectable(InjectionScope.Singleton) // Priority 2
class MyService {}
const MyModule = ProviderModule.create({
id: 'MyModule',
defaultScope: InjectionScope.Singleton, // Priority 3 (lowest)
providers: [
{
provide: MyService,
useClass: MyService,
scope: InjectionScope.Transient, // Priority 1 (highest) - WINS!
},
],
});
// Token scope wins: new instance every time
const s1 = MyModule.get(MyService);
const s2 = MyModule.get(MyService);
console.log(s1 === s2); // false[!IMPORTANT] Request scope is useful for scenarios like:
- HTTP request tracking (same request ID across all services in one request)
- Transaction contexts (same database transaction across all repositories)
- User context (same user data across all services in one operation)
Module System
Import/Export Pattern
Modules explicitly control dependency boundaries through imports and exports, providing encapsulation and clear interfaces.
@Injectable()
class DatabaseService {
query(sql: string) {
return [{ result: 'data' }];
}
}
@Injectable()
class InternalCacheService {
// Private to DatabaseModule
cache = new Map();
}
const DatabaseModule = ProviderModule.create({
id: 'DatabaseModule',
providers: [DatabaseService, InternalCacheService],
exports: [DatabaseService], // Only DatabaseService is accessible
});
const ApiModule = ProviderModule.create({
id: 'ApiModule',
imports: [DatabaseModule],
providers: [ApiService],
});
// ✅ Works - DatabaseService is exported
const dbService = ApiModule.get(DatabaseService);
// ❌ Error - InternalCacheService not exported
// const cache = ApiModule.get(InternalCacheService);Nested Imports:
const LayerA = ProviderModule.create({
id: 'LayerA',
providers: [ServiceA],
exports: [ServiceA],
});
const LayerB = ProviderModule.create({
id: 'LayerB',
imports: [LayerA],
providers: [ServiceB],
exports: [ServiceB, LayerA], // Re-export LayerA
});
const LayerC = ProviderModule.create({
id: 'LayerC',
imports: [LayerB],
});
// ✅ Works - ServiceA accessible through LayerB's re-export
const serviceA = LayerC.get(ServiceA);
// ✅ Works - ServiceB exported by LayerB
const serviceB = LayerC.get(ServiceB);Re-exporting Modules
Modules can re-export imported modules to create aggregation modules.
const DatabaseModule = ProviderModule.create({
id: 'DatabaseModule',
providers: [DatabaseService],
exports: [DatabaseService],
});
const ConfigModule = ProviderModule.create({
id: 'ConfigModule',
providers: [ConfigService],
exports: [ConfigService],
});
const LoggerModule = ProviderModule.create({
id: 'LoggerModule',
providers: [LoggerService],
exports: [LoggerService],
});
// CoreModule aggregates common modules
const CoreModule = ProviderModule.create({
id: 'CoreModule',
imports: [DatabaseModule, ConfigModule, LoggerModule],
exports: [DatabaseModule, ConfigModule, LoggerModule], // Re-export all
});
// Consumers import CoreModule and get all three modules
const FeatureModule = ProviderModule.create({
id: 'FeatureModule',
imports: [CoreModule], // Just import one module
});
// Access all re-exported providers
const db = FeatureModule.get(DatabaseService);
const config = FeatureModule.get(ConfigService);
const logger = FeatureModule.get(LoggerService);[!TIP] Create "barrel" or "core" modules that re-export commonly used modules to simplify imports throughout your application.
Dynamic Module Updates
Modules support runtime modifications for flexibility. Use sparingly as it can impact performance.
const DynamicModule = ProviderModule.create({
id: 'DynamicModule',
providers: [ServiceA],
});
// Add providers dynamically
DynamicModule.update.addProvider(ServiceB);
DynamicModule.update.addProvider(ServiceC, true); // true = also export
// Add imports dynamically
const DatabaseModule = ProviderModule.create({
id: 'DatabaseModule',
providers: [DatabaseService],
exports: [DatabaseService],
});
DynamicModule.update.addImport(DatabaseModule, true); // true = also export
// Check what's available
console.log(DynamicModule.hasProvider(ServiceB)); // true
console.log(DynamicModule.isImportingModule('DatabaseModule')); // true
console.log(DynamicModule.isExportingProvider(ServiceC)); // true
// Remove providers and imports
DynamicModule.update.removeProvider(ServiceB);
DynamicModule.update.removeImport(DatabaseModule);
DynamicModule.update.removeFromExports(ServiceC);Dynamic Import Propagation:
const ModuleA = ProviderModule.create({
id: 'ModuleA',
providers: [ServiceA],
exports: [ServiceA],
});
const ModuleB = ProviderModule.create({
id: 'ModuleB',
imports: [ModuleA],
exports: [ModuleA],
});
const ModuleC = ProviderModule.create({
id: 'ModuleC',
providers: [ServiceC],
exports: [ServiceC],
});
// Initially, ModuleB doesn't have ServiceC
console.log(ModuleB.hasProvider(ServiceC)); // false
// Dynamically import ModuleC into ModuleA and export it
ModuleA.update.addImport(ModuleC, true);
// Now ModuleB automatically has ServiceC (import propagation!)
console.log(ModuleB.hasProvider(ServiceC)); // true[!WARNING] Dynamic module updates:
- Can impact performance if used frequently
- Should be used primarily for testing or plugin systems
- May make dependency graphs harder to understand
- Are propagated automatically to importing modules
Global Modules
Declare a single AppBootstrapModule blueprint with isGlobal: true at your app's entry point. It is automatically imported into AppModule, making its exported providers available to every module without any explicit imports.
@Injectable()
class LoggerService {
log(message: string) {
console.log(`[LOG] ${message}`);
}
}
@Injectable()
class ConfigService {
apiUrl = 'https://api.example.com';
}
// Declare once at app entry point
ProviderModule.blueprint({
id: 'AppBootstrapModule',
isGlobal: true,
providers: [LoggerService, ConfigService],
exports: [LoggerService, ConfigService],
});
// Automatically imported into AppModule
console.log(AppModule.isImportingModule('AppBootstrapModule')); // true
// Every module can resolve these without importing anything
const FeatureModule = ProviderModule.create({ id: 'FeatureModule' });
const logger = FeatureModule.get(LoggerService); // Works![!CAUTION] Even if not enforced, it is recommended to keep
isGlobal: trueto a singleAppBootstrapModule. Multiple global modules create hidden implicit dependencies that are hard to trace. If a module is not truly app-wide, import it explicitly instead.
Dependency Injection
Constructor Injection
The primary way to inject dependencies. TypeScript metadata handles it automatically with @Injectable().
@Injectable()
class DatabaseService {
query(sql: string) {
return [{ data: 'result' }];
}
}
@Injectable()
class LoggerService {
log(message: string) {
console.log(message);
}
}
@Injectable()
class UserRepository {
// Dependencies automatically injected via constructor
constructor(
private readonly db: DatabaseService,
private readonly logger: LoggerService
) {}
findAll() {
this.logger.log('Finding all users');
return this.db.query('SELECT * FROM users');
}
}
const UserModule = ProviderModule.create({
id: 'UserModule',
providers: [DatabaseService, LoggerService, UserRepository],
});
// UserRepository automatically receives DatabaseService and LoggerService
const userRepo = UserModule.get(UserRepository);@Inject Decorator
Use @Inject for explicit injection when automatic resolution doesn't work (e.g., string tokens, interfaces).
import { Inject, Injectable } from '@adimm/x-injection';
@Injectable()
class ApiService {
constructor(
@Inject('API_KEY') private readonly apiKey: string,
@Inject('API_URL') private readonly apiUrl: string,
@Inject('MAX_RETRIES') private readonly maxRetries: number
) {}
makeRequest() {
console.log(`Calling ${this.apiUrl} with key ${this.apiKey}`);
console.log(`Max retries: ${this.maxRetries}`);
}
}
const ApiModule = ProviderModule.create({
id: 'ApiModule',
providers: [
{ provide: 'API_KEY', useValue: 'secret-123' },
{ provide: 'API_URL', useValue: 'https://api.example.com' },
{ provide: 'MAX_RETRIES', useValue: 3 },
ApiService,
],
});
const apiService = ApiModule.get(ApiService);
apiService.makeRequest();Injecting Abstract Classes:
@Injectable()
abstract class PaymentGateway {
abstract charge(amount: number): Promise<void>;
}
@Injectable()
class StripePaymentGateway extends PaymentGateway {
async charge(amount: number) {
console.log(`Stripe: Charging $${amount}`);
}
}
@Injectable()
class PaymentService {
constructor(@Inject(PaymentGateway) private readonly gateway: PaymentGateway) {}
async processPayment(amount: number) {
await this.gateway.charge(amount);
}
}
const PaymentModule = ProviderModule.create({
id: 'PaymentModule',
providers: [{ provide: PaymentGateway, useClass: StripePaymentGateway }, PaymentService],
});@MultiInject Decorator
Inject multiple providers bound to the same token as an array.
import { Injectable, MultiInject } from '@adimm/x-injection';
@Injectable()
class EmailNotifier {
notify() {
console.log('Email notification sent');
}
}
@Injectable()
class SmsNotifier {
notify() {
console.log('SMS notification sent');
}
}
@Injectable()
class PushNotifier {
notify() {
console.log('Push notification sent');
}
}
abstract class Notifier {
abstract notify(): void;
}
@Injectable()
class NotificationService {
constructor(@MultiInject(Notifier) private readonly notifiers: Notifier[]) {}
notifyAll() {
this.notifiers.forEach((notifier) => notifier.notify());
}
}
const NotificationModule = ProviderModule.create({
id: 'NotificationModule',
providers: [
{ provide: Notifier, useClass: EmailNotifier },
{ provide: Notifier, useClass: SmsNotifier },
{ provide: Notifier, useClass: PushNotifier },
NotificationService,
],
});
const service = NotificationModule.get(NotificationService);
service.notifyAll();
// Output:
// Email notification sent
// SMS notification sent
// Push notification sentAlternative with module.get():
const MyModule = ProviderModule.create({
id: 'MyModule',
providers: [
{ provide: 'Handler', useValue: 'Handler1' },
{ provide: 'Handler', useValue: 'Handler2' },
{ provide: 'Handler', useValue: 'Handler3' },
],
});
// Get all providers bound to 'Handler'
const handlers = MyModule.get('Handler', false, true); // (token, isOptional=false, asList=true)
console.log(handlers); // ['Handler1', 'Handler2', 'Handler3']Optional Dependencies
Use the isOptional flag to handle missing dependencies gracefully.
@Injectable()
class ServiceA {
value = 'A';
}
@Injectable()
class ServiceB {
constructor(
private serviceA: ServiceA,
@Inject('OPTIONAL_CONFIG') private readonly config?: any
) {}
}
const MyModule = ProviderModule.create({
id: 'MyModule',
providers: [ServiceA, ServiceB],
});
// Get with optional flag
const optionalService = MyModule.get('NOT_EXISTS', true); // isOptional = true
console.log(optionalService); // undefined (no error thrown)
// Without optional flag (throws error)
try {
const service = MyModule.get('NOT_EXISTS'); // Throws!
} catch (error) {
console.error('Provider not found');
}[!TIP] Use
@Injectwhen:
- Injecting string tokens or symbols
- Injecting abstract classes
- TypeScript's automatic injection doesn't work (interfaces, etc.)
Use
@MultiInjectwhen:
- You want to collect all providers bound to a single token
- Implementing plugin systems
- Working with strategy patterns
Lifecycle Hooks
Lifecycle hooks allow you to execute code at specific points in a module's lifecycle.
onReady Hook
Invoked immediately after module creation. Perfect for initialization logic.
@Injectable()
class DatabaseService {
connected = false;
async connect() {
console.log('Connecting to database...');
this.connected = true;
}
}
const DatabaseModule = ProviderModule.create({
id: 'DatabaseModule',
providers: [DatabaseService],
onReady: async (module) => {
console.log('DatabaseModule is ready!');
// Initialize services
const db = module.get(DatabaseService);
await db.connect();
console.log('Database connected:', db.connected);
},
});
// Output:
// DatabaseModule is ready!
// Connecting to database...
// Database connected: trueonReset Hook
Invoked when module.reset() is called. Provides before and after callbacks for cleanup and reinitialization.
@Injectable()
class CacheService {
cache = new Map();
clear() {
this.cache.clear();
}
}
const CacheModule = ProviderModule.create({
id: 'CacheModule',
providers: [CacheService],
onReset: () => {
return {
before: async (mod) => {
console.log('Before reset - clearing cache');
const cache = mod.get(CacheService);
cache.clear();
},
after: async () => {
console.log('After reset - cache reinitialized');
},
};
},
});
// Trigger reset
await CacheModule.reset();
// Output:
// Before reset - clearing cache
// After reset - cache reinitializedonDispose Hook
Invoked when module.dispose() is called. Perfect for cleanup tasks like closing connections.
@Injectable()
class DatabaseService {
connected = true;
async disconnect() {
console.log('Disconnecting from database...');
this.connected = false;
}
}
@Injectable()
class FileService {
async closeFiles() {
console.log('Closing open files...');
}
}
const AppModule = ProviderModule.create({
id: 'AppModule',
providers: [DatabaseService, FileService],
onDispose: () => {
return {
before: async (mod) => {
console.log('Cleanup started');
const db = mod.get(DatabaseService);
const files = mod.get(FileService);
await db.disconnect();
await files.closeFiles();
},
after: async () => {
console.log('Cleanup completed');
},
};
},
});
// Dispose module
await AppModule.dispose();
// Output:
// Cleanup started
// Disconnecting from database...
// Closing open files...
// Cleanup completed
// Module is now disposed
console.log(AppModule.isDisposed); // true
// Subsequent operations throw error
try {
AppModule.get(DatabaseService);
} catch (error) {
console.error('Cannot access disposed module');
}[!IMPORTANT] Lifecycle hook execution order:
- onReady - Immediately after module creation
- onReset (before) → module reset → onReset (after)
- onDispose (before) → module disposal → onDispose (after)
[!WARNING] After calling
dispose():
- All module operations will throw errors
- The module cannot be reused
- Internal resources are cleaned up
- Use for application shutdown or when modules are truly finished
Events System
The events system allows you to observe and react to module changes in real-time.
Subscribing to Events
import { DefinitionEventType } from '@adimm/x-injection';
const MyModule = ProviderModule.create({
id: 'MyModule',
providers: [ServiceA],
});
// Subscribe to all events
const unsubscribe = MyModule.update.subscribe(({ type, change }) => {
console.log(`Event: ${DefinitionEventType[type]}`, change);
});
// Trigger events
MyModule.update.addProvider(ServiceB); // Event: Provider
MyModule.update.addImport(OtherModule); // Event: Import
const service = MyModule.get(ServiceA); // Event: GetProvider
// Clean up
unsubscribe();Available Event Types
enum DefinitionEventType {
Noop, // No operation
Import, // Module/blueprint added
Provider, // Provider added
GetProvider, // Provider resolved
Export, // Export added
ExportModule, // Module added to exports
ExportProvider, // Provider added to exports
ImportRemoved, // Module removed
ProviderRemoved, // Provider removed
ExportRemoved, // Export removed
ExportModuleRemoved, // Module removed from exports
ExportProviderRemoved, // Provider removed from exports
}Event Use Cases
Monitoring Provider Resolution:
const MonitoredModule = ProviderModule.create({
id: 'MonitoredModule',
providers: [DatabaseService, CacheService],
});
MonitoredModule.update.subscribe(({ type, change }) => {
if (type === DefinitionEventType.GetProvider) {
console.log('Provider accessed:', change.constructor.name);
console.log('Access time:', new Date().toISOString());
}
});
// Logs access
const db = MonitoredModule.get(DatabaseService);
// Output: Provider accessed: DatabaseService
// Access time: 2024-01-15T10:30:00.000ZTracking Module Composition:
@Injectable()
class ServiceA {}
@Injectable()
class ServiceB {}
const DatabaseModule = ProviderModule.create({
id: 'DatabaseModule',
providers: [ServiceA],
exports: [ServiceA],
});
const RootModule = ProviderModule.create({
id: 'RootModule',
});
const compositionLog: string[] = [];
RootModule.update.subscribe(({ type, change }) => {
switch (type) {
case DefinitionEventType.Import:
compositionLog.push(`Imported: ${change.id}`);
break;
case DefinitionEventType.Provider:
const providerName = typeof change === 'function' ? change.name : change.provide;
compositionLog.push(`Added provider: ${providerName}`);
break;
case DefinitionEventType.Export:
compositionLog.push(`Exported: ${JSON.stringify(change)}`);
break;
}
});
RootModule.update.addImport(DatabaseModule);
RootModule.update.addProvider(ServiceA);
RootModule.update.addProvider(ServiceB, true);
console.log(compositionLog);
// [
// 'Imported: DatabaseModule',
// 'Added provider: ServiceA',
// 'Added provider: ServiceB',
// 'Exported: ServiceB'
// ]Debugging Dynamic Changes:
const DebugModule = ProviderModule.create({
id: 'DebugModule',
});
DebugModule.update.subscribe(({ type, change }) => {
const eventName = DefinitionEventType[type];
if (type === DefinitionEventType.ImportRemoved) {
console.warn(`⚠️ Module removed: ${change.id}`);
} else if (type === DefinitionEventType.ProviderRemoved) {
console.warn(`⚠️ Provider removed:`, change);
} else {
console.log(`✅ ${eventName}:`, change);
}
});
DebugModule.update.addProvider(ServiceA);
DebugModule.update.removeProvider(ServiceA);[!WARNING]
- Always call
unsubscribe()to prevent memory leaks- Events fire after middlewares have executed
- Event handlers are synchronous - avoid heavy operations
- High-frequency events (like
GetProvider) can impact performance
Middlewares
Middlewares intercept and transform module operations before they complete. They provide powerful customization capabilities.
BeforeGet Middleware
Transform provider values before they're returned to consumers.
import { MiddlewareType } from '@adimm/x-injection';
@Injectable()
class UserService {
getUser() {
return { id: 1, name: 'Alice' };
}
}
const MyModule = ProviderModule.create({
id: 'MyModule',
providers: [UserService],
});
// Wrap resolved providers with metadata
MyModule.middlewares.add(MiddlewareType.BeforeGet, (provider, token, inject) => {
// Return true to pass through unchanged
if (!(provider instanceof UserService)) return true;
// Transform the value
return {
timestamp: Date.now(),
instance: provider,
metadata: { cached: false },
};
});
const result = MyModule.get(UserService);
console.log(result);
// {
// timestamp: 1705320000000,
// instance: UserService { ... },
// metadata: { cached: false }
// }Conditional Transformation:
@Injectable()
class ServiceA {}
@Injectable()
class ServiceB {}
MyModule.middlewares.add(MiddlewareType.BeforeGet, (provider, token) => {
// Only transform ServiceA
if (provider instanceof ServiceA) {
return { wrapped: provider, type: 'A' };
}
// Pass through everything else unchanged
return true;
});
const serviceA = MyModule.get(ServiceA); // { wrapped: ServiceA, type: 'A' }
const serviceB = MyModule.get(ServiceB); // ServiceB (unchanged)Using inject() to avoid infinite loops:
@Injectable()
class LoggerService {
log(message: string) {
console.log(message);
}
}
@Injectable()
class PaymentService {}
MyModule.middlewares.add(MiddlewareType.BeforeGet, (provider, token, inject) => {
if (!(provider instanceof PaymentService)) return true;
// Use inject() instead of module.get() to avoid infinite loop
const logger = inject(LoggerService);
logger.log('Payment service accessed');
return provider; // Or transform it
});BeforeAddProvider Middleware
Block specific providers:
MyModule.middlewares.add(MiddlewareType.BeforeAddProvider, (provider) => {
// Block ServiceB from being added
if ((provider as any).name === 'ServiceB') {
return false; // Abort
}
return true; // Allow
});
MyModule.update.addProvider(ServiceA);
MyModule.update.addProvider(ServiceB); // Silently rejected
MyModule.update.addProvider(ServiceC);
console.log(MyModule.hasProvider(ServiceA)); // true
console.log(MyModule.hasProvider(ServiceB)); // false
console.log(MyModule.hasProvider(ServiceC)); // trueBeforeAddImport Middleware
Intercept modules before they're imported.
const Module1 = ProviderModule.create({ id: 'Module1' });
const Module2 = ProviderModule.create({ id: 'Module2' });
const RestrictedModule = ProviderModule.create({ id: 'RestrictedModule' });
const MainModule = ProviderModule.create({ id: 'MainModule' });
// Block specific modules
MainModule.middlewares.add(MiddlewareType.BeforeAddImport, (module) => {
if (module.id === 'RestrictedModule') {
console.warn(`❌ Cannot import ${module.id}`);
return false; // Block
}
return true; // Allow
});
MainModule.update.addImport(Module1); // ✅ Allowed
MainModule.update.addImport(Module2); // ✅ Allowed
MainModule.update.addImport(RestrictedModule); // ❌ Blocked
console.log(MainModule.isImportingModule('Module1')); // true
console.log(MainModule.isImportingModule('RestrictedModule')); // falseAuto-add providers to imported modules:
MyModule.middlewares.add(MiddlewareType.BeforeAddImport, (importedModule) => {
// Add logger to every imported module
importedModule.update.addProvider(LoggerService, true);
return importedModule; // Return modified module
});
MyModule.update.addImport(FeatureModule);
// FeatureModule now has LoggerServiceOnExportAccess Middleware
Control which importing modules can access exports.
@Injectable()
class SensitiveService {}
@Injectable()
class PublicService {}
const SecureModule = ProviderModule.create({
id: 'SecureModule',
providers: [SensitiveService, PublicService],
exports: [SensitiveService, PublicService],
});
// Restrict access based on importer
SecureModule.middlewares.add(MiddlewareType.OnExportAccess, (importerModule, exportToken) => {
// Block untrusted modules from accessing SensitiveService
if (importerModule.id === 'UntrustedModule' && exportToken === SensitiveService) {
console.warn(`❌ ${importerModule.id} denied access to SensitiveService`);
return false; // Deny
}
return true; // Allow
});
const TrustedModule = ProviderModule.create({
id: 'TrustedModule',
imports: [SecureModule],
});
const UntrustedModule = ProviderModule.create({
id: 'UntrustedModule',
imports: [SecureModule],
});
// Trusted module can access both
console.log(TrustedModule.hasProvider(SensitiveService)); // true
console.log(TrustedModule.hasProvider(PublicService)); // true
// Untrusted module blocked from SensitiveService
console.log(UntrustedModule.hasProvider(SensitiveService)); // false
console.log(UntrustedModule.hasProvider(PublicService)); // trueComplete access control:
SecureModule.middlewares.add(MiddlewareType.OnExportAccess, (importer, exportToken) => {
const allowlist = ['TrustedModule1', 'TrustedModule2'];
if (!allowlist.includes(String(importer.id))) {
console.warn(`Access denied for ${importer.id}`);
return false;
}
return true;
});BeforeRemoveImport Middleware
Prevent specific modules from being removed.
const PermanentModule = ProviderModule.create({ id: 'PermanentModule' });
const TemporaryModule = ProviderModule.create({ id: 'TemporaryModule' });
const MainModule = ProviderModule.create({ id: 'MainModule' });
// Protect PermanentModule
MainModule.middlewares.add(MiddlewareType.BeforeRemoveImport, (module) => {
if (module.id === 'PermanentModule') {
console.warn(`⚠️ Cannot remove ${module.id}`);
return false; // Block removal
}
return true; // Allow removal
});
MainModule.update.addImport(PermanentModule);
MainModule.update.addImport(TemporaryModule);
// Try to remove
MainModule.update.removeImport(PermanentModule); // ❌ Blocked
MainModule.update.removeImport(TemporaryModule); // ✅ Removed
console.log(MainModule.isImportingModule('PermanentModule')); // true
console.log(MainModule.isImportingModule('TemporaryModule')); // falseBeforeRemoveProvider Middleware
Prevent specific providers from being removed.
MyModule.middlewares.add(MiddlewareType.BeforeRemoveProvider, (provider) => {
// Block removal of critical services
if (provider === DatabaseService) {
console.warn('⚠️ Cannot remove DatabaseService');
return false;
}
return true;
});
MyModule.update.addProvider(DatabaseService);
MyModule.update.addProvider(CacheService);
MyModule.update.removeProvider(DatabaseService); // ❌ Blocked
MyModule.update.removeProvider(CacheService); // ✅ Removed
console.log(MyModule.hasProvider(DatabaseService)); // true
console.log(MyModule.hasProvider(CacheService)); // falseBeforeRemoveExport Middleware
Prevent specific exports from being removed.
import { ProviderModuleHelpers } from '@adimm/x-injection';
const MyModule = ProviderModule.create({
id: 'MyModule',
providers: [ServiceA, ServiceB],
exports: [ServiceA, ServiceB],
});
MyModule.middlewares.add(MiddlewareType.BeforeRemoveExport, (exportDef) => {
// Check if it's a module or provider
if (ProviderModuleHelpers.isModule(exportDef)) {
// Block module removal
return exportDef.id !== 'ProtectedModule';
} else {
// Block ServiceA removal
return exportDef !== ServiceA;
}
});
MyModule.update.removeFromExports(ServiceA); // ❌ Blocked
MyModule.update.removeFromExports(ServiceB); // ✅ Removed
console.log(MyModule.isExportingProvider(ServiceA)); // true
console.log(MyModule.isExportingProvider(ServiceB)); // falseAll Available Middleware Types
enum MiddlewareType {
BeforeAddImport, // Before importing a module
BeforeAddProvider, // Before adding a provider
BeforeGet, // Before returning provider to consumer
BeforeRemoveImport, // Before removing an import
BeforeRemoveProvider, // Before removing a provider
BeforeRemoveExport, // Before removing an export
OnExportAccess, // When importer accesses exports
}Middleware Return Values:
false- Abort the operation (block it)true- Pass through unchanged- Modified value - Transform and continue
- For
BeforeGet: Can return any value (transformation)
[!CAUTION] Middleware best practices:
- Returning
falseaborts the chain (no value returned)- Middlewares execute in registration order
- Always handle errors in middleware chains
- Use
inject()parameter in BeforeGet to avoid infinite loops- Be careful with performance - middlewares run on every operation
- Events fire after middlewares complete
Testing
xInjection makes testing easy through blueprint cloning and provider substitution.
Blueprint Cloning
Clone blueprints to create test-specific configurations without affecting production code.
// Production blueprint
const DatabaseModuleBp = ProviderModule.blueprint({
id: 'DatabaseModule',
providers: [DatabaseService, ConnectionPool],
exports: [DatabaseService],
});
// Test blueprint - clone and modify
const DatabaseModuleMock = DatabaseModuleBp.clone().updateDefinition({
id: 'DatabaseModuleMock',
providers: [
{ provide: DatabaseService, useClass: MockDatabaseService },
{ provide: ConnectionPool, useClass: MockConnectionPool },
],
});
// Use in tests
const TestModule = ProviderModule.create({
id: 'TestModule',
imports: [DatabaseModuleMock],
});
const db = TestModule.get(DatabaseService); // MockDatabaseServiceDeep Blueprint Cloning:
const OriginalBp = ProviderModule.blueprint({
id: 'Original',
providers: [ServiceA, ServiceB, ServiceC],
exports: [ServiceA, ServiceB],
onReady: (module) => console.log('Original ready'),
});
// Clone and completely override
const ClonedBp = OriginalBp.clone().updateDefinition({
id: 'Cloned',
providers: [MockServiceA, MockServiceB], // Different providers
exports: [MockServiceA], // Different exports
onReady: undefined, // Remove lifecycle hooks
});
// Original blueprint unchanged
console.log(OriginalBp.providers?.length); // 3
console.log(ClonedBp.providers?.length); // 2Provider Substitution
Replace real services with mocks for testing.
// Production services
@Injectable()
class ApiService {
async fetchData() {
return fetch('https://api.example.com/data').then((r) => r.json());
}
}
@Injectable()
class UserService {
constructor(private api: ApiService) {}
async getUsers() {
return this.api.fetchData();
}
}
// Mock service
class MockApiService {
async fetchData() {
return { users: [{ id: 1, name: 'Mock User' }] };
}
}
// Production module
const ProductionModule = ProviderModule.create({
id: 'ProductionModule',
providers: [ApiService, UserService],
});
// Test module with substitution
const TestModule = ProviderModule.create({
id: 'TestModule',
providers: [
{ provide: ApiService, useClass: MockApiService },
UserService, // Uses MockApiService automatically
],
});
const userService = TestModule.get(UserService);
const users = await userService.getUsers();
console.log(users); // Mock dataMocking Services
Using useValue for simple mocks:
const mockPaymentGateway = {
charge: jest.fn().mockResolvedValue({ success: true }),
refund: jest.fn().mockResolvedValue({ success: true }),
};
const TestModule = ProviderModule.create({
id: 'TestModule',
providers: [{ provide: PaymentGateway, useValue: mockPaymentGateway }, PaymentService],
});
const paymentService = TestModule.get(PaymentService);
await paymentService.processPayment(100);
expect(mockPaymentGateway.charge).toHaveBeenCalledWith(100);Using useFactory for complex mocks:
const TestModule = ProviderModule.create({
id: 'TestModule',
providers: [
{
provide: 'DATABASE_CONNECTION',
useFactory: () => {
return {
query: jest.fn().mockResolvedValue([{ id: 1, name: 'Test' }]),
connect: jest.fn().mockResolvedValue(true),
disconnect: jest.fn().mockResolvedValue(true),
};
},
},
],
});
const db = TestModule.get('DATABASE_CONNECTION');
const results = await db.query('SELECT * FROM users');
expect(results).toEqual([{ id: 1, name: 'Test' }]);Complete Testing Example:
// Production code
@Injectable()
class EmailService {
async sendEmail(to: string, subject: string, body: string) {
// Real email sending logic
console.log(`Sending email to ${to}`);
}
}
@Injectable()
class UserNotificationService {
constructor(private emailService: EmailService) {}
async notifyUser(userId: string, message: string) {
await this.emailService.sendEmail(`user${userId}@example.com`, 'Notification', message);
}
}
// Test code
describe('UserNotificationService', () => {
it('should send email notification', async () => {
const mockEmailService = {
sendEmail: jest.fn().mockResolvedValue(undefined),
};
const TestModule = ProviderModule.create({
id: 'TestModule',
providers: [{ provide: EmailService, useValue: mockEmailService }, UserNotificationService],
});
const notificationService = TestModule.get(UserNotificationService);
await notificationService.notifyUser('123', 'Test message');
expect(mockEmailService.sendEmail).toHaveBeenCalledWith('[email protected]', 'Notification', 'Test message');
});
});Testing with Multiple Module Layers:
// Create mock blueprint
const MockDataModuleBp = ProviderModule.blueprint({
id: 'MockDataModule',
providers: [
{ provide: DatabaseService, useClass: MockDatabaseService },
{ provide: CacheService, useClass: MockCacheService },
],
exports: [DatabaseService, CacheService],
});
// Use mock in feature module tests
const FeatureModuleTest = ProviderModule.create({
id: 'FeatureModuleTest',
imports: [MockDataModuleBp],
providers: [FeatureService],
});
const featureService = FeatureModuleTest.get(FeatureService);
// FeatureService receives mock dependencies[!TIP] Testing strategies:
- Use
blueprint.clone()to create test variations without modifying originals- Use
useValuefor simple mocks- Use
useClassfor class-based mocks with behavior- Use
useFactoryfor complex mock setup- Test module isolation by mocking all external dependencies
- Verify mock calls with your test framework
OOP-Style Modules with ProviderModuleClass
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.
Basic OOP Module
import { Injectable, ProviderModuleClass } from '@adimm/x-injection';
@Injectable()
class UserService {
get(id: string) {
return { id, name: 'John Doe' };
}
}
@Injectable()
class AuthService {
constructor(private readonly userService: UserService) {}
login(userId: string) {
const user = this.userService.get(userId);
return `Logged in as ${user.name}`;
}
}
class AuthModule extends ProviderModuleClass {
constructor() {
super({
id: 'AuthModule',
providers: [UserService, AuthService],
exports: [AuthService],
});
}
authenticateUser(userId: string): string {
return this.module.get(AuthService).login(userId);
}
// Custom method named 'get' - no conflict with module.get()!
get(): string {
return 'custom-get-value';
}
}
const authModule = new AuthModule();
console.log(authModule.authenticateUser('123')); // "Logged in as John Doe"
console.log(authModule.get()); // "custom-get-value"
// DI container always accessible via .module
authModule.module.update.addProvider(NewService);[!IMPORTANT] All
ProviderModulemethods are available through the.moduleproperty to prevent naming conflicts with your custom methods.
When to Use OOP vs Functional
OOP-style (extends ProviderModuleClass): when you need custom business logic methods, computed getters, initialization state, or want to prevent naming conflicts with the DI API.
Functional-style (ProviderModule.create()): when you only need a provider container with no extra behavior — simpler and more concise.
Both styles are fully compatible and can be mixed in the same application.
Advanced Module API
Query Methods
Check module state and relationships.
const MyModule = ProviderModule.create({
id: 'MyModule',
imports: [DatabaseModule, ConfigModule],
providers: [ServiceA, ServiceB],
exports: [ServiceA, DatabaseModule],
});
// Provider queries
MyModule.hasProvider(ServiceA); // true
MyModule.hasProvider(ServiceC); // false
MyModule.hasProvider(DatabaseService); // true (from import)
// Import queries
MyModule.isImportingModule('DatabaseModule'); // true
MyModule.isImportingModule(ConfigModule); // true (by reference)
MyModule.isImportingModule('NonExistent'); // false
// Export queries
MyModule.isExportingProvider(ServiceA); // true
MyModule.isExportingProvider(ServiceB); // false
MyModule.isExportingModule('DatabaseModule'); // true
MyModule.isExportingModule(ConfigModule); // false
// State queries
MyModule.isDisposed; // false
MyModule.id; // 'MyModule'Using Symbol Identifiers:
const MODULE_ID = Symbol('FeatureModule');
const FeatureModule = ProviderModule.create({
id: MODULE_ID,
providers: [FeatureService],
exports: [FeatureService],
});
const AppModule = ProviderModule.create({
id: 'AppModule',
imports: [FeatureModule],
});
// Query using Symbol
console.log(AppModule.isImportingModule(MODULE_ID)); // trueBatch Resolution with getMany()
Resolve multiple providers in a single call.
@Injectable()
class ServiceA {
name = 'A';
}
@Injectable()
class ServiceB {
name = 'B';
}
@Injectable()
class ServiceC {
name = 'C';
}
const MyModule = ProviderModule.create({
id: 'MyModule',
providers: [
ServiceA,
ServiceB,
ServiceC,
{ provide: 'CONFIG_A', useValue: 'config-a' },
{ provide: 'CONFIG_B', useValue: 'config-b' },
],
});
// Simple getMany
const [serviceA, serviceB, configA] = MyModule.getMany(ServiceA, ServiceB, 'CONFIG_A');
console.log(serviceA.name); // 'A'
console.log(serviceB.name); // 'B'
console.log(configA); // 'config-a'With Options:
// Optional providers
const [serviceA, missing, serviceC] = MyModule.getMany(
ServiceA,
{ provider: 'NON_EXISTENT', isOptional: true },
ServiceC
);
console.log(serviceA); // ServiceA instance
console.log(missing); // undefined (no error)
console.log(serviceC); // ServiceC instance
// Get as list (multiple bindings)
const HandlerModule = ProviderModule.create({
id: 'HandlerModule',
providers: [
{ provide: 'Handler', useValue: 'H1' },
{ provide: 'Handler', useValue: 'H2' },
{ provide: 'Handler', useValue: 'H3' },
],
});
const [handlers] = HandlerModule.getMany({
provider: 'Handler',
asList: true,
});
console.log(handlers); // ['H1', 'H2', 'H3']Complex Example:
const [database, cache, optionalLogger, allPlugins, config] = MyModule.getMany(
DatabaseService,
CacheService,
{ provider: LoggerService, isOptional: true },
{ provider: Plugin, asList: true },
'APP_CONFIG'
);
// All providers resolved in one call
// optionalLogger is undefined if not available
// allPlugins is an array of all Plugin bindings[!IMPORTANT] >
getMany()parameter types:
- Simple: Just pass the token directly
- With options: Use object with
provider,isOptional, and/orasList
Hierarchical Dependency Injection
When a module resolves a provider via module.get(), xInjection walks up a well-defined lookup chain until it finds a binding or throws.
Resolution order (highest to lowest priority):
- Own container — providers declared directly in this module
- Imported modules — exported providers from each module in the
importsarray (in order) - AppModule — globally available providers (from
isGlobal: truemodules)
module.get(SomeService)
│
▼
┌──────────────────────┐
│ Own container │ ← providers: [SomeService, ...]
│ (highest priority) │
└──────────┬───────────┘
│ not found
▼
┌──────────────────────┐
│ Imported modules │ ← imports: [DatabaseModule, ConfigModule]
│ (exported only) │ DatabaseModule.exports: [DatabaseService]
└──────────┬───────────┘ ConfigModule.exports: [ConfigService]
│ not found
▼
┌──────────────────────┐
│ AppModule │ ← AppBootstrapModule { isGlobal: true }
│ (lowest priority) │ exports: [LoggerService, ...]
└──────────┬───────────┘
│ not found
▼
InjectionProviderModuleMissingProviderErrorResolution in practice — given the modules set up in Import/Export Pattern and Global Modules:
ApiModule.get(ApiService); // ✅ ① own container
ApiModule.get(ConfigService); // ✅ ② ConfigModule export
ApiModule.get(DatabaseService); // ✅ ② DatabaseModule export
ApiModule.get(LoggerService); // ✅ ③ AppModule (via AppBootstrapModule)
ApiModule.get(InternalCache); // ❌ not exported → MissingProviderError[!TIP] A provider that isn't in a module's
exportsis completely invisible to any importer — it's a private implementation detail. Think ofexportsas the public API of a module.
Resources
📚 Full API Documentation - Complete TypeDoc reference
⚛️ React Integration - Official React hooks and providers
💡 GitHub Issues - Bug reports and feature requests
🌟 GitHub Repository - Source code and examples
Contributing
Contributions are welcome! Please ensure code follows the project style guidelines and includes appropriate tests.
- Fork the repository
- Create a feature branch
- Make your changes with tests
- Submit a pull request
Credits
Author: Adi-Marian Mutu
Built on: InversifyJS
Logo: Alexandru Turica
License
MIT © Adi-Marian Mutu
