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.
Modern applications face several dependency management challenges. Let's examine these problems and how xInjection solves them.
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 sites
With 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!
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 code
With 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 Stripe
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 InjectionProviderModuleMissingProviderError
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 leaks
With 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 leaks
xInjection is a Dependency Injection library built on InversifyJS, inspired by NestJS's modular architecture. It solves the pain points above through:
onReady, onReset, onDispose for module lifecycle managementProviderModuleClass for class-based module architecturenpm 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 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"
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' };
}
}
The @Injectable() decorator is required for any class that:
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 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:
Use blueprints when you need the same module configuration in multiple places, or when you want to delay module creation until runtime.
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 Singleton inside a blueprint are only singletons relative to the module that imported them, not globally. If ModuleA and ModuleB both import ConfigModuleBp, they each get their own ConfigService singleton — the two instances are completely independent of each other.
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.
id: 'AppModule' is reserved — you cannot create your own module with that nameAppModule into other modulesSee Global Modules for full usage and best practices.
xInjection supports four types of provider tokens, each serving different use cases.
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);
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); // MockPaymentGateway
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 }],
});
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'
Use factory tokens when:
Control provider lifecycle with three scope types. Scope priority order: token scope > decorator scope > module default scope.
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); // true
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); // false
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); // false
Visual 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) ──┘
Scopes are resolved in the following priority order (highest to lowest):
@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
Request scope is useful for scenarios like:
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);
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);
Create "barrel" or "core" modules that re-export commonly used modules to simplify imports throughout your application.
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
Dynamic module updates:
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!
Even if not enforced, it is recommended to keep isGlobal: true to a single AppBootstrapModule. Multiple global modules create hidden implicit dependencies that are hard to trace. If a module is not truly app-wide, import it explicitly instead.
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);
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],
});
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 sent
Alternative 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']
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');
}
Use @Inject when:
Use @MultiInject when:
Lifecycle hooks allow you to execute code at specific points in a module's lifecycle.
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: true
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 reinitialized
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');
}
Lifecycle hook execution order:
After calling dispose():
The events system allows you to observe and react to module changes in real-time.
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();
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
}
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.000Z
Tracking 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);
unsubscribe() to prevent memory leaksGetProvider) can impact performanceMiddlewares intercept and transform module operations before they complete. They provide powerful customization capabilities.
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
});
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)); // true
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')); // false
Auto-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 LoggerService
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)); // true
Complete 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;
});
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')); // false
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)); // false
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)); // false
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 unchangedBeforeGet: Can return any value (transformation)Middleware best practices:
false aborts the chain (no value returned)inject() parameter in BeforeGet to avoid infinite loopsxInjection makes testing easy through blueprint cloning and provider substitution.
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); // MockDatabaseService
Deep 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); // 2
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 data
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('user123@example.com', '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
Testing strategies:
blueprint.clone() to create test variations without modifying originalsuseValue for simple mocksuseClass for class-based mocks with behavioruseFactory for complex mock setupFor 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 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);
All ProviderModule methods are available through the .module property to prevent naming conflicts with your custom methods.
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.
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)); // true
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
> getMany() parameter types:
provider, isOptional, and/or asListWhen 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):
imports array (in order)isGlobal: true modules)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
▼
InjectionProviderModuleMissingProviderError
Resolution 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
A provider that isn't in a module's exports is completely invisible to any importer — it's a private implementation detail. Think of exports as the public API of a module.
📚 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
Contributions are welcome! Please ensure code follows the project style guidelines and includes appropriate tests.
Author: Adi-Marian Mutu
Built on: InversifyJS
Logo: Alexandru Turica
MIT © Adi-Marian Mutu