xInjection - v3.0.0
    Preparing search index...

    xInjection Logo


    xInjection is a powerful Dependency Injection library built on InversifyJS, inspired by NestJS's modular architecture. It provides fine-grained control over dependency encapsulation through a module-based system where each module manages its own container with explicit import/export boundaries.

    • Modular Architecture - NestJS-style import/export system for clean dependency boundaries
    • Isolated Containers - Each module manages its own InversifyJS container
    • Flexible Scopes - Singleton, Transient, and Request-scoped providers
    • Lazy Loading - Blueprint pattern for deferred module instantiation
    • Lifecycle Hooks - onReady, onReset, onDispose for module lifecycle management
    • Events & Middlewares - Deep customization through event subscriptions and middleware chains
    • Framework Agnostic - Works in Node.js and browser environments
    • TypeScript First - Full type safety with decorator support
    npm install @adimm/x-injection reflect-metadata
    

    TypeScript Configuration (tsconfig.json):

    {
    "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
    }
    }

    Import reflect-metadata at your application's entry point:

    import 'reflect-metadata';
    
    import { Injectable, ProviderModule } from '@adimm/x-injection';

    @Injectable()
    class UserService {
    getUser(id: string) {
    return { id, name: 'John Doe' };
    }
    }

    @Injectable()
    class AuthService {
    constructor(private userService: UserService) {}

    login(userId: string) {
    const user = this.userService.getUser(userId);
    return `Logged in as ${user.name}`;
    }
    }

    const AuthModule = ProviderModule.create({
    id: 'AuthModule',
    providers: [UserService, AuthService],
    exports: [AuthService],
    });

    const authService = AuthModule.get(AuthService);
    console.log(authService.login('123')); // "Logged in as John Doe"

    For developers who prefer class-based architecture, xInjection provides ProviderModuleClass - a composition-based wrapper that prevents naming conflicts between your custom methods and the DI container methods.

    import { Injectable, ProviderModuleClass } from '@adimm/x-injection';

    @Injectable()
    class UserService {
    get(id: string) {
    return { id, name: 'John Doe' };
    }
    }

    @Injectable()
    class AuthService {
    constructor(private userService: UserService) {}

    login(userId: string) {
    const user = this.userService.get(userId);
    return `Logged in as ${user.name}`;
    }
    }

    // OOP-style module extending ProviderModuleClass
    class AuthModule extends ProviderModuleClass {
    constructor() {
    super({
    id: 'AuthModule',
    providers: [UserService, AuthService],
    exports: [AuthService],
    });
    }

    authenticateUser(userId: string): string {
    const authService = this.module.get(AuthService);
    return authService.login(userId);
    }

    getUserById(userId: string) {
    const userService = this.module.get(UserService);
    return userService.get(userId);
    }
    }

    // Instantiate and use
    const authModule = new AuthModule();
    console.log(authModule.authenticateUser('123')); // "Logged in as John Doe"

    // All ProviderModule methods are available through the `.module` property
    const authService = authModule.module.get(AuthService);
    authModule.module.update.addProvider(NewService);

    Module with Initialization Logic:

    class DatabaseModule extends ProviderModuleClass {
    private isConnected = false;

    constructor() {
    super({
    id: 'DatabaseModule',
    providers: [DatabaseService, ConnectionPool],
    exports: [DatabaseService],
    onReady: async (module) => {
    console.log('DatabaseModule ready!');
    },
    });
    }

    async connect(): Promise<void> {
    const dbService = this.module.get(DatabaseService);
    await dbService.connect();
    this.isConnected = true;
    }

    getConnectionStatus(): boolean {
    return this.isConnected;
    }
    }

    const dbModule = new DatabaseModule();
    await dbModule.connect();
    console.log(dbModule.getConnectionStatus()); // true

    Module with Computed Properties:

    class ApiModule extends ProviderModuleClass {
    constructor() {
    super({
    id: 'ApiModule',
    imports: [ConfigModule, LoggerModule],
    providers: [ApiService, HttpClient],
    exports: [ApiService],
    });
    }

    // Computed property - lazy evaluation
    get apiService(): ApiService {
    return this.module.get(ApiService);
    }

    get httpClient(): HttpClient {
    return this.module.get(HttpClient);
    }

    // Business logic using multiple services
    async makeAuthenticatedRequest(url: string, token: string) {
    const client = this.httpClient;
    return client.request(url, {
    headers: { Authorization: `Bearer ${token}` },
    });
    }
    }

    const apiModule = new ApiModule();
    const response = await apiModule.makeAuthenticatedRequest('/users', 'token');

    Module Composition:

    class BaseModule extends ProviderModuleClass {
    protected logAction(action: string): void {
    const logger = this.module.get(LoggerService);
    logger.log(`[${String(this.module.id)}] ${action}`);
    }
    }

    class UserModule extends BaseModule {
    constructor() {
    super({
    id: 'UserModule',
    providers: [UserService, UserRepository],
    exports: [UserService],
    });
    }

    createUser(name: string) {
    this.logAction(`Creating user: ${name}`);
    const userService = this.module.get(UserService);
    return userService.create(name);
    }

    deleteUser(id: string) {
    this.logAction(`Deleting user: ${id}`);
    const userService = this.module.get(UserService);
    return userService.delete(id);
    }
    }

    Use OOP-style (extends ProviderModuleClass) when:

    • You need custom business logic methods on the module itself
    • You prefer class-based architecture
    • You want computed properties or getters for providers
    • You need initialization logic or state management in the module
    • You're building a complex module with multiple related operations

    Use Functional-style (ProviderModule.create()) when:

    • You only need dependency injection without custom logic
    • You prefer functional composition
    • You want simpler, more concise code
    • You're creating straightforward provider containers

    Key Point: Both styles are fully compatible and can be mixed within the same application. ProviderModuleClass uses composition (contains a ProviderModule as this.module), preventing method name conflicts while providing identical DI functionality.

    The fundamental building block of xInjection. Similar to NestJS modules, each ProviderModule encapsulates related providers with explicit control over what's exposed.

    const DatabaseModule = ProviderModule.create({
    id: 'DatabaseModule',
    imports: [ConfigModule], // Modules to import
    providers: [DatabaseService], // Services to register
    exports: [DatabaseService], // What to expose to importers
    });

    Key Methods:

    • Module.get(token) - Resolve a provider instance
    • Module.update.addProvider() - Dynamically add providers
    • Module.update.addImport() - Import other modules at runtime
    • Module.dispose() - Clean up module resources

    Full API Documentation →

    The global root module, automatically available in every application. Global modules are auto-imported into AppModule.

    import { AppModule } from '@adimm/x-injection';

    // Add global providers
    AppModule.update.addProvider(LoggerService);

    // Access from any module
    const anyModule = ProviderModule.create({ id: 'AnyModule' });
    const logger = anyModule.get(LoggerService);

    Blueprints allow you to define module configurations without instantiating them, enabling lazy loading and template reuse.

    // Define blueprint
    const DatabaseModuleBp = ProviderModule.blueprint({
    id: 'DatabaseModule',
    providers: [DatabaseService],
    exports: [DatabaseService],
    });

    // Import blueprint (auto-converts to module)
    const AppModule = ProviderModule.create({
    id: 'AppModule',
    imports: [DatabaseModuleBp],
    });

    // Or create module from blueprint later
    const DatabaseModule = ProviderModule.create(DatabaseModuleBp);

    Benefits:

    • Deferred instantiation for better startup performance
    • Reusable module templates across your application
    • Scoped singletons per importing module

    xInjection supports four types of provider tokens:

    1. Class Token (simplest):

    @Injectable()
    class ApiService {}

    providers: [ApiService];

    2. Class Token with Substitution:

    providers: [{ provide: ApiService, useClass: MockApiService }];
    

    3. Value Token (constants):

    providers: [{ provide: 'API_KEY', useValue: 'secret-key-123' }];
    

    4. Factory Token (dynamic):

    providers: [
    {
    provide: 'DATABASE_CONNECTION',
    useFactory: (config: ConfigService) => createConnection(config.dbUrl),
    inject: [ConfigService],
    },
    ];

    Control provider lifecycle with three scope types (priority order: token > decorator > module default):

    Cached after first resolution - same instance every time:

    @Injectable() // Singleton by default
    class DatabaseService {}

    Module.get(DatabaseService) === Module.get(DatabaseService); // true

    New instance on every resolution:

    @Injectable(InjectionScope.Transient)
    class RequestLogger {}

    Module.get(RequestLogger) === Module.get(RequestLogger); // false

    Single instance per resolution tree (useful for request-scoped data):

    @Injectable(InjectionScope.Request)
    class RequestContext {}

    @Injectable(InjectionScope.Transient)
    class Controller {
    constructor(
    public ctx1: RequestContext,
    public ctx2: RequestContext
    ) {}
    }

    const controller = Module.get(Controller);
    controller.ctx1 === controller.ctx2; // true (same resolution)

    const controller2 = Module.get(Controller);
    controller.ctx1 === controller2.ctx1; // false (different resolution)

    Setting Scopes:

    // 1. In provider token (highest priority)
    providers: [{ provide: Service, useClass: Service, scope: InjectionScope.Transient }];

    // 2. In @Injectable decorator
    @Injectable(InjectionScope.Request)
    class Service {}

    // 3. Module default (lowest priority)
    ProviderModule.create({
    id: 'MyModule',
    defaultScope: InjectionScope.Transient,
    });

    Modules explicitly control dependency boundaries through imports and exports:

    const DatabaseModule = ProviderModule.create({
    id: 'DatabaseModule',
    providers: [DatabaseService, InternalCacheService],
    exports: [DatabaseService], // Only DatabaseService is accessible
    });

    const ApiModule = ProviderModule.create({
    id: 'ApiModule',
    imports: [DatabaseModule], // Gets access to DatabaseService
    providers: [ApiService],
    });

    // ✅ Works
    const dbService = ApiModule.get(DatabaseService);

    // ❌ Error - InternalCacheService not exported
    const cache = ApiModule.get(InternalCacheService);

    Modules can re-export imported modules to create aggregation modules:

    const CoreModule = ProviderModule.create({
    id: 'CoreModule',
    imports: [DatabaseModule, ConfigModule],
    exports: [DatabaseModule, ConfigModule], // Re-export both
    });

    // Consumers get both DatabaseModule and ConfigModule
    const AppModule = ProviderModule.create({
    imports: [CoreModule],
    });

    Modules support runtime modifications (use sparingly for performance):

    const module = ProviderModule.create({ id: 'DynamicModule' });

    // Add providers dynamically
    module.update.addProvider(NewService);
    module.update.addProvider(AnotherService, true); // true = also export

    // Add imports dynamically
    module.update.addImport(DatabaseModule, true); // true = also export

    Important: Dynamic imports propagate automatically - if ModuleA imports ModuleB, and ModuleB dynamically imports ModuleC (with export), ModuleA automatically gets access to ModuleC's exports.

    Mark modules as global to auto-import into AppModule:

    const LoggerModule = ProviderModule.create({
    id: 'LoggerModule',
    isGlobal: true,
    providers: [LoggerService],
    exports: [LoggerService],
    });

    // LoggerService now available in all modules without explicit import
    Warning

    These features provide deep customization but can add complexity. Use them only when necessary.

    Subscribe to module lifecycle events for monitoring and debugging:

    import { DefinitionEventType } from '@adimm/x-injection';

    const module = ProviderModule.create({
    id: 'MyModule',
    providers: [MyService],
    });

    const unsubscribe = module.update.subscribe(({ type, change }) => {
    if (type === DefinitionEventType.GetProvider) {
    console.log('Provider resolved:', change);
    }
    if (type === DefinitionEventType.Import) {
    console.log('Module imported:', change);
    }
    });

    // Clean up when done
    unsubscribe();

    Available Events: GetProvider, Import, Export, AddProvider, RemoveProvider, ExportModule - Full list →

    Warning

    Always unsubscribe to prevent memory leaks. Events fire after middlewares.

    Intercept and transform provider resolution before values are returned:

    import { MiddlewareType } from '@adimm/x-injection';

    const module = ProviderModule.create({
    id: 'MyModule',
    providers: [PaymentService],
    });

    // Transform resolved values
    module.middlewares.add(MiddlewareType.BeforeGet, (provider, token, inject) => {
    // Pass through if not interested
    if (!(provider instanceof PaymentService)) return true;

    // Use inject() to avoid infinite loops
    const logger = inject(LoggerService);
    logger.log('Payment service accessed');

    // Transform the value
    return {
    timestamp: Date.now(),
    value: provider,
    };
    });

    const payment = module.get(PaymentService);
    // { timestamp: 1234567890, value: PaymentService }

    Control export access:

    module.middlewares.add(MiddlewareType.OnExportAccess, (importerModule, exportToken) => {
    // Restrict access based on importer
    if (importerModule.id === 'UntrustedModule' && exportToken === SensitiveService) {
    return false; // Deny access
    }
    return true; // Allow
    });

    Available Middlewares: BeforeGet, BeforeAddProvider, BeforeAddImport, OnExportAccess - Full list →

    Caution

    • Returning false aborts the chain (no value returned)
    • Returning true passes value unchanged
    • Middlewares execute in registration order
    • Always handle errors in middleware chains

    Create mock modules easily using blueprint cloning:

    // Production module
    const ApiModuleBp = ProviderModule.blueprint({
    id: 'ApiModule',
    providers: [UserService, ApiService],
    exports: [ApiService],
    });

    // Test module - clone and override
    const ApiModuleMock = ApiModuleBp.clone().updateDefinition({
    id: 'ApiModuleMock',
    providers: [
    { provide: UserService, useClass: MockUserService },
    {
    provide: ApiService,
    useValue: {
    sendRequest: jest.fn().mockResolvedValue({ data: 'test' }),
    },
    },
    ],
    });

    // Use in tests
    const testModule = ProviderModule.create({
    imports: [ApiModuleMock],
    });

    📚 Full API Documentation - Complete TypeDoc reference

    ⚛️ React Integration - Official React hooks and providers

    💡 GitHub Issues - Bug reports and feature requests

    Contributions welcome! Please ensure code follows the project style guidelines.

    Author: Adi-Marian Mutu Built on: InversifyJS Logo: Alexandru Turica

    MIT © Adi-Marian Mutu