xInjection - v2.1.2
    Preparing search index...

    xInjection Logo


    xInjection is a robust Inversion of Control (IoC) library that extends InversifyJS with a modular, NestJS-inspired Dependency Injection (DI) system. It enables you to encapsulate dependencies with fine-grained control using ProviderModule classes, allowing for clean separation of concerns and scalable architecture.

    Each ProviderModule manages its own container, supporting easy decoupling and explicit control over which providers are exported and imported across modules. The global AppModule is always available, ensuring a seamless foundation for your application's DI needs.

    • NestJS-inspired module system: Import and export providers between modules.
    • Granular dependency encapsulation: Each module manages its own container.
    • Flexible provider scopes: Singleton, Request, and Transient lifecycles.
    • Lifecycle hooks: onReady, onReset and onDispose for module initialization and cleanup.
    • Middlewares: Tap into the low-level implementation without any effort by just adding new middlewares.
    • Events: Subscribe to internal events for maximum control.
    • Blueprints: Plan ahead your modules without eagerly instantiating them.
    • Fully Agnostic: It doesn't rely on any framework, just on InversifyJS as it uses it under-the-hood to build the containers. It works the same both client side and server side.

    First, ensure you have reflect-metadata installed:

    npm i reflect-metadata
    
    Note

    You may have to add import 'reflect-metadata' at the entry point of your application.

    Then install xInjection:

    npm i @adimm/x-injection
    

    Add the following options to your tsconfig.json to enable decorator metadata:

    {
    "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
    }
    }
    import { Injectable, ProviderModule } from '@adimm/x-injection';

    @Injectable()
    class HelloService {
    sayHello() {
    return 'Hello, world!';
    }
    }

    const HelloModule = ProviderModule.create({
    id: 'HelloModule',
    providers: [HelloService],
    exports: [HelloService],
    });

    const helloService = HelloModule.get(HelloService);

    console.log(helloService.sayHello());
    // => 'Hello, world!'

    The core class of xInjection, if you ever worked with NestJS (or Angular), you'll find it very familiar.

    const GarageModule = ProviderModule.create({ id: 'GarageModule', imports: [], providers: [], exports: [] });
    

    It is a special instance of the ProviderModule class which acts as the root of your modules graph, all global modules will be automatically imported into the AppModule and shared across all your modules.

    Another core class which most probably you'll end using a lot too, to keep it short, it allows you to plan ahead the modules without instantiating them.

    const CarModuleBlueprint = ProviderModule.blueprint({ id: 'CarModule', imports: [], providers: [], exports: [] });
    

    It is used to refer to the three main blocks of a module:

    The library has some opinionated naming conventions which you should adopt too

    All variables holding an instance of a ProviderModule should be written in PascalCase and suffixed with Module, like this:

    const DatabaseModule = ProviderModule.create({...});
    const UserModule = ProviderModule.create({...});
    const CarPartsModule = ProviderModule.create({...});

    The id property of the ProviderModule.options should be the same as the module variable name.

    const DatabaseModule = ProviderModule.create({ id: 'DatabaseModule' });
    const UserModule = ProviderModule.create({ id: 'UserModule' });
    const CarPartsModule = ProviderModule.create({ id: 'CarPartsModule' });

    If you are exporting a module from a designated file, then you should name that file as following:

    database.module.ts
    user.module.ts
    car-parts.module.ts
    Tip

    If you install/use the Material Icon Theme VS Code extension, you'll see the *.module.ts files with a specific icon.

    All variables holding an instance of a ProviderModuleBlueprint should be written in PascalCase too and suffixed with ModuleBp, like this:

    const DatabaseModuleBp = ProviderModule.blueprint({...});
    const UserModuleBp = ProviderModule.blueprint({...});
    const CarPartsModuleBp = ProviderModule.blueprint({...});

    The id property of the ProviderModuleBlueprint.options should not end with Bp because when you'll import that blueprint into a module, the exact provided id will be used!

    const DatabaseModuleBp = ProviderModule.create({ id: 'DatabaseModule' });
    const UserModuleBp = ProviderModule.create({ id: 'UserModule' });
    const CarPartsModuleBp = ProviderModule.create({ id: 'CarPartsModule' });

    If you are exporting a blueprint from a designated file, then you should name that file as following:

    database.module.bp.ts
    user.module.bp.ts
    car-parts.module.bp.ts

    All variables holding an object representing a ProviderToken should be written in SCREAMING_SNAKE_CASE and suffixed with _PROVIDER, like this:

    const USER_SERVICE_PROVIDER = UserService;
    

    If you are exporting a provider token from a designated file, then you should name that file as following:

    user-service.provider.ts
    

    As explained above, it is the root module of your application, it is always available and eagerly bootstrapped.

    Usually you'll not interact much with it as any module which is defined as global will be automatically imported into it, therefore having its exports definition available across all your modules out-of-the-box. However, you can use it like any ProviderModule instance.

    Warning

    Importing the AppModule into any module will throw an error!

    You have 2 options to access it:

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

    or

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

    ProviderModule.APP_MODULE_REF;

    // This option is mostly used internally, but you can 100% safely use it as well.

    Providing global services to the AppModule:

    @Injectable()
    class UserService {}

    AppModule.update.addProvider(UserService);
    Note

    All providers scope is set to Singleton by default if not provided.

    Yes, that's it, now you have access to the UserService anywhere in your app across your modules, meaning that you can now do:

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

    const userService = UnrelatedModule.get(UserService);
    // returns the `userService` singleton instance.

    You can see all the available properties and methods of the ProviderModule here.

    There are mainly 3 first-class ways to set the InjectionScope of a provider, and each one has an order priority. The below list shows them in order of priority (highest to lowest), meaning that if 2 (or more) ways are used, the method with the highest priority will take precedence.

    1. By providing the scope property to the ProviderToken:
      const USER_PROVIDER: ProviderToken<UserService> = {
      scope: InjectionScope.Request,
      provide: UserService,
      useClass: UserService,
      };
    2. Within the @Injectable decorator:
      @Injectable(InjectionScope.Transient)
      class Transaction {}
    3. By providing the defaultScope property when initializing a ProviderModule:
      const RainModuleDef = new ProviderModuleDef({
      id: 'RainModule',
      defaultScope: InjectionScope.Transient,
      });
    Note

    Imported modules/providers retain their original InjectionScope!

    The Singleton injection scope means that once a dependency has been resolved from within a module will be cached and further resolutions will use the value from the cache.

    Example:

    expect(MyModule.get(MyProvider)).toBe(MyModule.get(MyProvider));
    // true

    The Transient injection scope means that a new instance of the dependency will be used whenever a resolution occurs.

    Example:

    expect(MyModule.get(MyProvider)).toBe(MyModule.get(MyProvider));
    // false

    The Request injection scope means that the same instance will be used when a resolution happens in the same request scope.

    Example:

    @Injectable(InjectionScope.Transient)
    class Book {
    author: string;
    }

    @Injectable(InjectionScope.Request)
    class Metro2033 extends Book {
    override author = 'Dmitry Alekseyevich Glukhovsky';
    }

    @Injectable(InjectionScope.Transient)
    class Library {
    constructor(
    public readonly metro2033: Metro2033,
    public readonly metro2033_reference: Metro2033
    ) {}
    }

    const winstonLibrary = MyModule.get(Library);
    const londonLibrary = MyModule.get(Library);

    expect(winstonLibrary.metro2033).toBe(winstonLibrary.metro2033_reference);
    expect(londonLibrary.metro2033).toBe(londonLibrary.metro2033_reference);
    // true

    expect(winstonLibrary.metro2033).toBe(londonLibrary.metro2033);
    // false

    A ProviderToken is another core block of xInjection (and also many other IoC/DI libs) which is used to define a token which can then be used to resolve a provider.

    xInjection offers 4 types of tokens:

    • ProviderIdentifier

      • It allows you to bind a value to a specific transparent token, like a Class, Function, symbol or string:
      const API_SERVICE_PROVIDER = ApiService;
      // or
      const CONSTANT_SECRET_PROVIDER = 'Shh';
    • ProviderClassToken

      • It can be used define the token and the provider:
      const HUMAN_SERVICE_PROVIDER = { provide: HumanService, useClass: FemaleService };

      // This will bind `HumanService` as the `token` and will resolve `FemaleService` from the container.
    • ProviderValueToken

      • It can be used to easily bind constant values, it can be anything, but once resolved it'll be cached and re-used upon further resolutions
      const THEY_DONT_KNOW_PROVIDER = { provide: CONSTANT_SECRET_PROVIDER, useValue: `They'll never know` };
      const THEY_MAY_KNOW_PROVIDER = { provide: CONSTANT_SECRET_PROVIDER, useValue: 'Maybe they know?' };

      // As you can see we now have 2 different ProviderTokens which use the same `provide` key.
      // This means that resolving the `CONSTANT_SECRET_PROVIDER` will return an array of strings.
    • ProviderFactoryToken

      • It can be used to bind a factory which is intended for more complex scenarios:
      const MAKE_PIZZA_PROVIDER = {
      provide: 'MAKE_PIZZA',
      useFactory: async (apiService: ApiService, pizzaService: PizzaService) => {
      const typeOfPizza = await apiService.getTypeOfPizza();

      if (typeOfPizza === 'margherita') return pizzaService.make.margherita;
      if (typeOfPizza === 'quattro_stagioni') return pizzaService.make.quattroStagioni;
      // and so on
      },
      // optional
      inject: [API_SERVICE_PROVIDER, PizzaService],
      };

    These are all the available ProviderToken you can use.

    Note

    In NestJS and Angular you can't use a ProviderToken to get a value, xInjection allows this pattern, but you must understand that what it actually does, is to use the value from the provide property.

    As you already saw till here, everything relies around the ProviderModule class, so let's dive a little more deep into understanding it.

    The most straight forward way to create/instantiate a new module is:

    const MyModule = ProviderModule.create({
    id: 'MyModule',
    imports: [AnotherModule, SecondModule, ThirdModule],
    providers: [
    { provide: CONSTANT_SECRET_PROVIDER, useValue: 'ultra secret' },
    PizzaService,
    { provide: HumanService, useClass: FemaleService },
    ],
    exports: [SecondModule, ThirdModule, PizzaService],
    });

    From what we can see, the MyModule is importing into it 3 more modules, each of them may export one or more (maybe nothing, that's valid too) providers, or even other modules. Because we imported them into the MyModule, now we have access to any providers they may have chosen to export, and the same is true also for their exported modules.

    Then, we've chosen to re-export from the MyModule the SecondModule and ThirdModule, meaning that if a different module imports MyModule, it'll automatically get access to those 2 modules as well. And in the end we also exported our own PizzaService, while the remaining other 2 providers, CONSTANT_SECRET_PROVIDER and HumanService can't be accessed when importing MyModule.

    This is the core feature of xInjection (and Angular/NestJS DI system), being able to encapsulate the providers, so nothing can spill out without our explicit consent.


    We could also achieve the above by using the ProviderModule API like this:

    MyModule.update.addImport(AnotherModule);
    MyModule.update.addImport(SecondModule, true); // `true` means "also add to the `exports` definition"
    MyModule.update.addImport(ThirdModule, true);

    MyModule.update.addProvider({ provide: CONSTANT_SECRET_PROVIDER, useValue: 'ultra secret' });
    MyModule.update.addProvider(PizzaService, true);
    MyModule.update.addProvider({ provide: HumanService, useClass: FemaleService });

    Now you may probably ask yourself If we import with the 'addImport' method a new module into an already imported module, will we have access to the providers of that newly imported module?

    The ansuwer is yes, we do have access thanks to the dynamic nature of the ProviderModule class. Meaning that doing the following will work as expected:

    const InnerModule = ProviderModule.create({
    id: 'InnerModule',
    providers: [FirstService],
    exports: [FirstService],
    });

    const OuterModule = ProviderModule.create({
    id: 'OuterModule',
    imports: [InnerModule],
    });

    const UnknownModule = ProviderModule.create({
    id: 'UnknownModule',
    providers: [SecondService],
    exports: [SecondService],
    });

    InnerModule.update.addImport(UnknownModule, true); // Don't forget to provide `true` to the `addToExports` optional parameter!

    const secondService = OuterModule.get(SecondService);

    The OuterModule has now access to the UnknownModule exports because it has been dynamically imported (later at run-time) into the InnerModule (which has been imported into OuterModule during the bootstrap phase)

    Basically what happens is that when a module is imported, it takes care of notify the host module if its definiton changed.

    Warning

    This is a very powerful feature which comes in with some costs, most of the time negligible, but if you have an app which has thousand and thousand of modules doing this type of dynamic behavior, you may incur in some performance issues which will require proper design to keep under control.

    Most of the times the best solution is to leverage the nature of blueprints.


    Sometimes you may actually want to lazy import a module from a file, this can be done very easily with xInjection:

    (async () => {
    await MyModule.update.addImportLazy(async () => (await import('./lazy.module')).LazyModule);

    MyModule.isImportingModule('LazyModule');
    // => true
    })();
    Tip

    This design pattern is extremely powerful and useful when you may have a lot of modules initializing during the app bootstrap process as you can defer their initialization, or even never load them if the user never needs those specific modules (this is mostly applicable on the client-side rather than the server-side)

    Keep reading to understand how you can defer initialization of the modules by using blueprints.

    The ProviderModuleBlueprint class main purpose is to encapsulate the definitions of a Module, when you do ProviderModule.blueprint({...}) you are not actually creating an instance of the ProviderModule class, but an instance of the ProviderModuleBlueprint class.

    To better understand the above concept; imagine the blueprint as being a dormant (static) module which is not fully awake (dynamic) till it is actually imported into a module.

    Whenever you import a blueprint into a module, it'll automatically be "transformed" to a ProviderModule instance by the engine, this step is crucial as a blueprint per se does not contain a container, just its definitions.

    Note

    Therefore it is important to understand the injection scope of an imported blueprint; we previously learned that when we import a blueprint into a module it automatically creates an instance of the ProviderModule from it, this means that all the singleton providers of the blueprint definition are now scoped singleton, where scoped means singleton in relation to their imported module.

    When you initialize a blueprint with the isGlobal property set to true, the out-of-the-box behavior is to automatically import the blueprint into the AppModule. You can disable this behavior by setting the autoImportIntoAppModuleWhenGlobal property to false

    const GlobalModuleBp = ProviderModule.blueprint({..., isGlobal: true }, { autoImportIntoAppModuleWhenGlobal: false });
    

    Now you can decide when to import it into the AppModule by doing AppModule.addImport(GlobalModuleBp).


    I highly recommend to take advantage of the blueprints nature in order to plan-ahead your modules;

    Why?

    • To define module configurations upfront without incurring the cost of immediate initialization (even if negligible).
    • To reuse module definitions across different parts of your application while maintaining isolated instances. (when possible/applicable)
    • To compose modules flexibly, allowing you to adjust module dependencies dynamically before instantiation.

    After you have provided the initial definitons of a blueprint, you can always modify them with the updateDefinition method.

    Note

    Updating the definitions of a blueprint after has been imported into a module will not propagate those changes to the module where it has been imported.


    This means that we can actually leverage the blueprints nature to defer the actual initialization of a module by doing so:

    const UserModuleBp = ProviderModule.blueprint({
    id: 'UserModule',
    ...
    });

    // Later in your code

    const UserModule = ProviderModule.create(UserModuleBp);

    The UserModule will be created only when necessary and it'll use the same exact definitons which are available into the UserModuleBp at the time of the create invokation.

    Warning

    This section covers advanced features which may add additional complexity (or even bugs) to your application if you misuse them, use these features only if truly needed and after evaluating the pros and cons of each.

    Each module will emit specific events through its life-cycle and you can intercept them by using the Module.update.subscribe method.

    Tip

    Here you can see all the available events

    If you'd need to intercept a get request, you can achieve that by doing:

    const CarModule = ProviderModule.create({
    id: 'CarModule',
    providers: [CarService],
    });

    CarModule.update.subscribe(({ type, change }) => {
    // We are interested only in the `GetProvider` event.
    if (type !== DefinitionEventType.GetProvider) return;

    // As our `CarModule` has only one provider, it is safe to assume
    // that the `change` will always be the `CarService` instance.
    const carService = change as CarService;

    console.log('CarService: ', carService);
    });

    const carService = CarModule.get(CarService);
    // logs => CarService: <instance_of_car_service_here>
    Warning

    After subscribing to a ProviderModule signal emission, you should make sure to also unsubscribe if you don't need anymore to intercept the changes, not doing so may cause memory leaks if you have lots of subscriptions which do heavy computations!

    The subscribe method will always return a method having the signature () => void, when you invoke it, it'll close the pipe which intercepts the signal emitted by the module:

    const unsubscribe = CarModule.update.subscribe(({ type, change }) => {
    /* heavy computation here */
    });

    // later in your code

    unsubscribe();
    Note

    Events are always invoked after middlewares

    Using middlewares is not encouraged as it allows you to tap into very deep low-level code which can cause unexpected bugs if not implemented carefully, however, middlewares are the perfect choice if you want to extend/alter the standard behavior of module as it allows you to decide what should happen with a resolved value before it is returned to the consumer.

    Tip

    Here you can see all the available middlewares

    Let's say that you want to wrap all the returned values of a specific module within an object having this signature { timestamp: number; value: any }. By using the GetProvider event will not do the trick because it doesn't allow you to alter/change the actual returned value to the consumer, you can indeed alter the content via reference, but not the actual result.

    So the easiest way to achieve that is by using the BeforeGet middleware as shown below:

    const TransactionModule = ProviderModule.create(TransactionModuleBp);

    TransactionModule.middlewares.add(MiddlewareType.BeforeGet, (provider, providerToken, inject) => {
    // We are interested only in the `providers` instances which are from the `Payment` class
    if (!(provider instanceof Payment)) return true;
    // or
    if (providerToken !== 'LAST_TRANSACTION') return true;

    // DON'T do this as you'll encounter an infinite loop
    const transactionService = TransactionModule.get(TransactionService);
    // If you have to inject into the middleware `context` from the `module`
    // use the `inject` parameter
    const transactionService = inject(TransactionService);

    return {
    timestamp: transactionService.getTimestamp(),
    value: provider,
    };
    });

    const transaction = TransactionModule.get('LAST_TRANSACTION');
    // transaction => { timestamp: 1363952948, value: <Payment_instance> }

    One more example is to add a middleware in order to dynamically control which modules can import a specific module by using the OnExportAccess flag.

    const UnauthorizedBranchBankModule = ProviderModule.create({ id: 'UnauthorizedBranchBankModule' });
    const SensitiveBankDataModule = ProviderModule.create({
    id: 'SensitiveBankDataModule',
    providers: [SensitiveBankDataService, NonSensitiveBankDataService],
    exports: [SensitiveBankDataService, NonSensitiveBankDataService],
    });

    SensitiveBankDataModule.middlewares.add(MiddlewareType.OnExportAccess, (importerModule, currentExport) => {
    // We want to deny access to our `SensitiveBankDataService` from the `exports` definition if the importer module is `UnauthorizedBranchBankModule`
    if (importerModule.toString() === 'UnauthorizedBranchBankModule' && currentExport === SensitiveBankDataService)
    return false;

    // Remaining module are able to import all our `export` definition
    // The `UnauthorizedBranchBankModule` is unable to import the `SensitiveBankDataService`
    return true;
    });
    Caution

    Returning false in a middleware will abort the chain, meaning that for the above example, no value would be returned. If you have to explicitly return a false boolean value, you may have to wrap your provider value as an workaround. (null is accepted as a return value)

    Meanwhile returning true means "return the value without changing it".

    In the future this behavior may change, so if your business logic relies a lot on middlewares make sure to stay up-to-date with the latest changes.

    It is also worth mentioning that you can apply multiple middlewares by just invoking the middlewares.add method multiple times, they are executed in the same exact order as you applied them, meaning that the first invokation to middlewares.add will actually be the root of the chain.

    If no error is thrown down the chain, all the registered middleware callback will be supplied with the necessary values.

    Warning

    It is the developer responsability to catch any error down the chain!

    If you are not interested in understanding how xInjection works under the hood, you can skip this section 😌

    It is the head of everything, a ProviderModule is actually composed by several classes, each with its own purpose.

    Tip

    You can get access to all the internal instances by doing new ProviderModule({...}) instead of ProviderModule.create({...})

    It is the class which takes care of managing the registered middlewares, check it out here.

    Not much to say about it as its main role is to register and build the middleware chain.

    It is the class which takes care of managing the inversify container, check it out here.

    Its main purpose is to initialize the module raw (InversifyJS Container) class and to bind the providers to it.

    It is the class which takes care of managing the imported modules, check it out here.

    Because modules can be imported into other modules, therefore creating a complex graph of modules, the purpose of this class is to keep track and sync the changes of the exports definition of the imported module.

    The ProviderModule API is simple yet very powerful, you may not realize that doing addImport will cause (based on how deep is the imported module) a chain reaction which the ImportedModuleContainer must keep track of in order to make sure that the consumer module which imported the consumed module has access only to the providers/modules explicitly exported by the consumed module.

    Therefore it is encouraged to keep things mostly static, as each addProvider, addImport, removeImport and so on have a penality cost on your application performance. This cost in most cases is negligible, however it highly depends on how the developer uses the feature xInjection offers.

    "With great power comes great responsibility."

    It is the class which takes care of managing the updates and event emissions of the module, check it out here.

    This class is actually the "parent" of the ImportedModuleContainer instances, its purpose is to build the initial definition graph, and while doing so it also instantiate for each imported module a new ImportedModuleContainer.

    It also take care of managing the events bubbling by checking cirular references and so on.

    It's the "metadata" counterpart of the ProviderModule class, as its only purpose is to carry the definitions. Check it out here.

    The library does also export a set of useful helpers in the case you may need it:

    import { ProviderModuleHelpers, ProviderTokenHelpers } from '@adimm/x-injection';
    

    This covers pretty much everything about how xInjection is built and how it works.

    It is very easy to create mock modules so you can use them in your unit tests.

    class ApiService {
    constructor(private readonly userService: UserService) {}

    async sendRequest<T>(location: LocationParams): Promise<T> {
    // Pseudo Implementation
    return this.sendToLocation(user, location);
    }

    private async sendToLocation(user: User, location: any): Promise<any> {}
    }

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

    // Clone returns a `deep` clone and wraps all the `methods` to break their reference!
    const ApiModuleBpMocked = ApiModuleBp.clone().updateDefinition({
    id: 'ApiModuleMocked',
    providers: [
    {
    provide: UserService,
    useClass: UserService_Mock,
    },
    {
    provide: ApiService,
    useValue: {
    sendRequest: async (location) => {
    console.log(location);
    },
    },
    },
    ],
    });

    Now what you have to do is just to provide the ApiModuleBpMocked instead of the ApiModuleBp 😎

    Comprehensive, auto-generated documentation is available at:

    👉 https://adimarianmutu.github.io/x-injection/index.html

    You want to use it within a ReactJS project? Don't worry, the library does already have an official implementation for React ⚛️

    For more details check out the GitHub Repository.

    Pull requests are warmly welcomed! 😃

    Please ensure your contributions adhere to the project's code style. See the repository for more details.


    Note

    For questions, feature requests, or bug reports, feel free to open an issue on GitHub.