Nexus-IoC is a familiar stranger in the world of TypeScript and DI

Background

In one of my projects, we used the Inversify library for dependency injection (DI). Although this is a powerful and flexible solution, its excessive flexibility eventually backfired on us: dependency management became increasingly confusing as the application grew. With each new module or component, the code became more complex, and the refactoring process became more and more painful.

I have identified several key requirements that I would like to see in the new solution:

  • Dependency transparency: It was necessary to clearly understand what dependencies each component required, without unnecessary magic in the code.

  • Hierarchy: It was important to maintain a strict structure, where modules and dependencies are clearly organized and easy to manage.

  • Extensibility: The code should remain easily extensible without the need to rewrite existing parts.

  • Early detection of errors: Catch errors at the development stage, not during application execution.

After exploring popular solutions like Angular and NestJS, I realized that these frameworks offer great dependency management capabilities, but they are too tightly integrated into their ecosystem, making them difficult to use outside of that context. I needed something universal. This is how the idea of ​​Nexus-IoC was born – a lightweight and flexible tool for managing dependencies in any TypeScript projects.

how the neuron sees my library

how the neuron sees my library

Nexus-IoC

First, let's look at a simple application example to get acquainted with the library. If you've worked with Angular or NestJS before, this code will be very familiar to you.

import { NsModule, Injectable } from '@nexus-ioc/core';
import { NexusApplicationsBrowser } from '@nexus-ioc/core/dist/server';


// Деклорация модуля
@Injectable()
class AppService {}

@NsModule({
  providers: [AppService]
})
class AppModule {}
// Деклорация модуля


// Точка старта приложения
async function bootstrap() {
  const app = await NexusApplicationsBrowser
    .create(AppModule)
    .bootstrap();
}

bootstrap();

Basic Concepts

  1. Modular architecture
    In Nexus-IoC, all application logic is organized around modules – isolated units of code that can include providers (dependencies) and other modules. This helps structure the application and make dependency management easier.

  2. Providers and dependencies
    Providers are objects that can be embedded in other parts of the application. Each module registers its own providers, and the system automatically resolves dependencies between them, which simplifies the implementation logic.

  3. Dependency graph
    When an application starts, Nexus-IoC automatically builds a dependency graph between modules and providers. This helps you see what dependencies each module requires and find errors during the build phase, such as circular dependencies or missing providers.

  4. Asynchronous loading of modules
    Nexus-IoC supports asynchronous loading of modules, which helps optimize application performance. Only the necessary parts of the code are loaded at the right time, which is especially important for the performance of large applications.

  5. Plugins to expand functionality
    The plugin system makes it easy to add new features without changing the core library. For example, you can connect plugins to visualize a dependency graph or for static code analysis.

Implementation of modules and providers

The core concept of Nexus-IoC is a module. Using a decorator @NsModulewhich takes three key parameters, you can declare a module:

  • imports — a list of modules used within the current one.

  • providers — list of providers provided by this module.

  • exports — list of providers available for other modules.

Provider types

  • UseClass provider – Provides a class for instantiating a dependency.

    {
      provide: "classProvider",
      useClass: class ClassProvider {}
    }
  • Class provider is a simple provider that registers a class.

    @Injectable()
    class Provider {}
  • UseValue provider – provides a specific value or object.

    {
      provide: "value-token",
      useValue: 'value'
    }
  • UseFactory provider – allows you to create dependencies through a factory function.

    {
      provide: "factory-token",
      useFactory: () => {
          // Поддерживается синхронный так и асинхронный вариант фабрики
      },
    },

Checking the integrity of the dependency graph

Nexus-IoC checks the integrity of the dependency graph before the application is launched. Similar to NestJS, the library analyzes the dependency graph and identifies problems such as circular dependencies or missing providers. But in Nexus-IoC, this process is more flexible: the graph is built taking into account the restrictions between modules, and actual dependency instances are created only when they are accessed.

This image was generated using the nexus-ioc-graph-visualizer plugin, which automatically visualizes your application's dependency graph.

This image was generated using the plugin nexus-ioc-graph-visualizerwhich automatically visualizes your application's dependency graph.

Nexus-IoC also provides a list of errors, which allows you to proactively detect problems before launching the application.

async function bootstrap() {
  const app = await NexusApplicationsBrowser
    .create(AppModule)
    .bootstrap();

  console.log(app.errors) // Здесь хранятся ошибки обнаруженные при построении графа
}

bootstrap();

Testing

The package was implemented @nexus-ioc/testingwhich greatly simplifies the process of testing containers and their components. With its help, you can quite easily write unit tests for modules and/or providers.

import { Injectable } from '@nexus-ioc/core';
import { Test } from '@nexus-ioc/testing';

describe('AppModule', () => {
  it('should create AppService instance', async () => {
    @Injectable()
    class AppService {}

    const appModule = await Test.createModule({
      providers: [AppService],
    }).compile();

    const appService = await appModule.get<AppService>(AppService);
    expect(appService).toBeInstanceOf(AppService);
  });
});
Replacing dependencies within a service
import { Injectable, Inject } from '@nexus-ioc/core';
import { Test } from '@nexus-ioc/testing';

describe('AppModule', () => {
  it('should create AppService instance', async () => {
    const MOCK_SECRET_KEY = 'secret-key'
    @Injectable()
    class AppService {
      constructor(@Inject('secret-key') public readonly secretKey: string) {}
    }

    const appModule = await Test.createModule({
      providers: [AppService, { provide: 'secret-key', useValue: MOCK_SECRET_KEY }],
    }).compile();

    const appService = await appModule.get<AppService>(AppService);
    expect(appService?.secretKey).toEqual(MOCK_SECRET_KEY);
  });
});

Reusability

Nexus-IoC implements familiar techniques for creating reusable modules − forRoot And forFeature. They allow you to flexibly configure modules depending on the needs of the application.

Differences between forRoot and forFeature

  • forRoot: These methods register providers at the global level. They are especially useful for services that must be available in any application module.

  • forFeature: These methods register providers only within the current module, making them ideal for local or specialized services.

Usage example

You can use forRootto register global services such as logging, and forFeature for local handlers that are needed only in specific modules.

Example forRoot module
import { NsModule, Injectable, DynamicModule } from '@nexus-ioc/core';

interface ConfigOptions {
  apiUrl: string;
}

// Сервис настроек
@Injectable()
class ConfigService {
  async getOptions(): Promise<ConfigOptions> {
    // симулируем загрузку данных из API
    return new Promise((resolve) => {
      setTimeout(() => resolve({ apiUrl: 'https://api.async.example.com' }), 1000);
    });
  }
}

@NsModule()
export class ConfigModule {
  // Обьявления модуля для глобального инстанцирования
  static forRoot(): DynamicModule {
    return {
      module: ConfigModule,
      providers: [
        ConfigService,
        {
          provide: 'CONFIG_OPTIONS',
          // Поддерживаются синхронные и асинхронные обьявления фабрик
          useFactory: (configService: ConfigService) =>
            configService.getOptions(),
          inject: [ConfigService], // Описываем зависимости фабрики
        },
      ],
      exports: ['CONFIG_OPTIONS'],
    };
  }
  // Обьявление модуля для локального инстанцирования
  static forFeature(): DynamicModule {
    return {
      module: ConfigModule,
      providers: [
        ConfigService,
        {
          provide: 'CONFIG_OPTIONS',
          useFactory: (configService: ConfigService) =>
            configService.getOptions(),
          inject: [ConfigService], // Описываем зависимости фабрики
        },
      ],
      exports: ['CONFIG_OPTIONS'],
    };
  }
}

Plugins to expand functionality

One of the important features Nexus-IoC is the ability to expand functionality using plugins. They allow you to add new features without changing the core library code.

One example is integration with a tool for analyzing and visualizing a dependency graph.

To do this, Nexus-IoC provides a method addScannerPluginwith which you can connect plugins at the stage of scanning the dependency graph. This method allows the integration of third-party tools that can interact with the graph during its construction.

How addScannerPlugin works

Method addScannerPlugin accepts the plugin as a function that will be called after the dependency graph construction stage. The plugin receives information about the graph, its nodes and edges. It is possible to implement additional check or modify the graph.

The first plugin that was created is GraphScannerVisualizer . Its task is to visualize the graph.

import { NexusApplicationsServer } from '@nexus-ioc/core/dist/server';
import { GraphScannerVisualizer } from 'nexus-ioc-graph-visualizer';
import { AppModule } from './apps'; 

// Добавляем плагин визуализации
const visualizer = new GraphScannerVisualizer("./graph.png");

async function bootstrap() {
	await NexusApplicationsServer.create(AppModule)
		.addScannerPlugin(visualizer)
		.bootstrap();
}

bootstrap();

Comparison with other options

Example on Nexus-IoC
import { Injectable, NsModule, Scope } from '@nexus-ioc/core';
import { NexusApplicationsServer } from '@nexus-ioc/core/dist/server'; 

@Injectable({ scope: Scope.Singleton })
class LoggerService {
  log(message: string) {
    console.log(message);
  }
}

@Injectable()
class UserService {
  constructor(private logger: LoggerService) {}

  printUser(userId: string) {
    this.logger.log(`logger: ${userId}`);
  }
}

@NsModule({
  providers: [LoggerService, UserService],
})
class AppModule {}

async function bootstrap() {
  const container = new NexusApplicationsServe.create(AppModule).bootstrap();
  const userService = await container.get<UserService>(UserService);
  
  userService.printUser('log me!');
}

bootstrap();
example on inversify
import 'reflect-metadata';
import { Container, injectable, inject } from 'inversify';

@injectable()
class LoggerService {
  log(message: string) {
    console.log(message);
  }
}

@injectable()
class UserService {
  constructor(@inject(LoggerService) private logger: LoggerService) {}

  printUser(userId: string) {
    this.logger.log(`User ID: ${userId}`);
  }
}

const container = new Container();
container.bind(LoggerService).toSelf();
container.bind(UserService).toSelf();

const userService = container.get(UserService);
userService.printUser('123');
example on Tsyringe:
import 'reflect-metadata';
import { container, injectable } from 'tsyringe';

@injectable()
class LoggerService {
  log(message: string) {
    console.log(message);
  }
}

@injectable()
class UserService {
  constructor(private logger: LoggerService) {}

  printUser(userId: string) {
    this.logger.log(`User ID: ${userId}`);
  }
}

container.registerSingleton(LoggerService);
container.registerSingleton(UserService);

const userService = container.resolve(UserService);
userService.printUser('123');

As you can see, there is no wunderwaffle here that would change the rules of the game and destroy competitors, the library manages dependencies, just doing it a little differently. The main difference from other solutions is that the declarative declaration of modules opens up great opportunities for static code analysiswhich helps during development large applications.

Finally

Who will benefit from this solution: Nexus-IoC is particularly well suited for large applications (enterprise level), where not only dependency management is important, but also clarity of application structure. I would not recommend this solution for small and medium-sized applications – here you can easily do without DI, especially in the initial stages. However, when the project becomes large-scale, with dozens of developers and teams interacting through contracts, Nexus-IoC can alleviate many of the problems associated with dependency management while providing powerful tools for code maintenance and analysis.

Plans:

  • The API is already stable and will not changebut there is still work to be done on optimization and full test coverage to bring the library to version 1.0

  • Development CLI to simplify working with the library

  • Creation static dependency graph analyzerto identify errors before the assembly stage

  • Development plugins for IDE to improve integration with editors

  • Improved documentation and site creation for the convenience of developers

Repository link: https://github.com/Isqanderm/ioc

Link to npm packages: https://www.npmjs.com/settings/nexus-ioc/packages

Github Wiki: https://github.com/Isqanderm/ioc/wiki

Nexus-IoC

First, let's look at a simple application example to get acquainted with the library. If you've worked with Angular or NestJS before, this code will be very familiar to you.

import { NsModule, Injectable } from '@nexus-ioc/core';
import { NexusApplicationsBrowser } from '@nexus-ioc/core/dist/server';


// Деклорация модуля
@Injectable()
class AppService {}

@NsModule({
  providers: [AppService]
})
class AppModule {}
// Деклорация модуля


// Точка старта приложения
async function bootstrap() {
  const app = await NexusApplicationsBrowser
    .create(AppModule)
    .bootstrap();
}

bootstrap();

Basic Concepts

  1. Modular architecture
    In Nexus-IoC, all application logic is organized around modules – isolated units of code that can include providers (dependencies) and other modules. This helps structure the application and make dependency management easier.

  2. Providers and dependencies
    Providers are objects that can be embedded in other parts of the application. Each module registers its own providers, and the system automatically resolves dependencies between them, which simplifies the implementation logic.

  3. Dependency graph
    When an application starts, Nexus-IoC automatically builds a dependency graph between modules and providers. This helps you see what dependencies each module requires and find errors during the build phase, such as circular dependencies or missing providers.

  4. Asynchronous loading of modules
    Nexus-IoC supports asynchronous loading of modules, which helps optimize application performance. Only the necessary parts of the code are loaded at the right time, which is especially important for the performance of large applications.

  5. Plugins to expand functionality
    The plugin system makes it easy to add new features without changing the core library. For example, you can connect plugins to visualize a dependency graph or for static code analysis.

Implementation of modules and providers

The core concept of Nexus-IoC is a module. Using a decorator @NsModulewhich takes three key parameters, you can declare a module:

  • imports — a list of modules used within the current one.

  • providers — list of providers provided by this module.

  • exports — list of providers available for other modules.

Provider types

  • UseClass provider – Provides a class for instantiating a dependency.

    {
      provide: "classProvider",
      useClass: class ClassProvider {}
    }
  • Class provider is a simple provider that registers a class.

    @Injectable()
    class Provider {}
  • UseValue provider – provides a specific value or object.

    {
      provide: "value-token",
      useValue: 'value'
    }
  • UseFactory provider – allows you to create dependencies through a factory function.

    {
      provide: "factory-token",
      useFactory: () => {
          // Поддерживается синхронный так и асинхронный вариант фабрики
      },
    },

Checking the integrity of the dependency graph

Nexus-IoC checks the integrity of the dependency graph before the application is launched. Similar to NestJS, the library analyzes the dependency graph and identifies problems such as circular dependencies or missing providers. But in Nexus-IoC, this process is more flexible: the graph is built taking into account the restrictions between modules, and actual dependency instances are created only when they are accessed.

This image was generated using the nexus-ioc-graph-visualizer plugin, which automatically visualizes your application's dependency graph.

This image was generated using the plugin nexus-ioc-graph-visualizerwhich automatically visualizes your application's dependency graph.

Nexus-IoC also provides a list of errors, which allows you to proactively detect problems before launching the application.

async function bootstrap() {
  const app = await NexusApplicationsBrowser
    .create(AppModule)
    .bootstrap();

  console.log(app.errors) // Здесь хранятся ошибки обнаруженные при построении графа
}

bootstrap();

Testing

The package was implemented @nexus-ioc/testingwhich greatly simplifies the process of testing containers and their components. With its help, you can quite easily write unit tests for modules and/or providers.

import { Injectable } from '@nexus-ioc/core';
import { Test } from '@nexus-ioc/testing';

describe('AppModule', () => {
  it('should create AppService instance', async () => {
    @Injectable()
    class AppService {}

    const appModule = await Test.createModule({
      providers: [AppService],
    }).compile();

    const appService = await appModule.get<AppService>(AppService);
    expect(appService).toBeInstanceOf(AppService);
  });
});
Replacing dependencies within a service
import { Injectable, Inject } from '@nexus-ioc/core';
import { Test } from '@nexus-ioc/testing';

describe('AppModule', () => {
  it('should create AppService instance', async () => {
    const MOCK_SECRET_KEY = 'secret-key'
    @Injectable()
    class AppService {
      constructor(@Inject('secret-key') public readonly secretKey: string) {}
    }

    const appModule = await Test.createModule({
      providers: [AppService, { provide: 'secret-key', useValue: MOCK_SECRET_KEY }],
    }).compile();

    const appService = await appModule.get<AppService>(AppService);
    expect(appService?.secretKey).toEqual(MOCK_SECRET_KEY);
  });
});

Reusability

Nexus-IoC implements familiar techniques for creating reusable modules − forRoot And forFeature. They allow you to flexibly configure modules depending on the needs of the application.

Differences between forRoot and forFeature

  • forRoot: These methods register providers at the global level. They are especially useful for services that must be available in any application module.

  • forFeature: These methods register providers only within the current module, making them ideal for local or specialized services.

Usage example

You can use forRootto register global services such as logging, and forFeature for local handlers that are needed only in specific modules.

Example forRoot module
import { NsModule, Injectable, DynamicModule } from '@nexus-ioc/core';

interface ConfigOptions {
  apiUrl: string;
}

// Сервис настроек
@Injectable()
class ConfigService {
  async getOptions(): Promise<ConfigOptions> {
    // симулируем загрузку данных из API
    return new Promise((resolve) => {
      setTimeout(() => resolve({ apiUrl: 'https://api.async.example.com' }), 1000);
    });
  }
}

@NsModule()
export class ConfigModule {
  // Обьявления модуля для глобального инстанцирования
  static forRoot(): DynamicModule {
    return {
      module: ConfigModule,
      providers: [
        ConfigService,
        {
          provide: 'CONFIG_OPTIONS',
          // Поддерживаются синхронные и асинхронные обьявления фабрик
          useFactory: (configService: ConfigService) =>
            configService.getOptions(),
          inject: [ConfigService], // Описываем зависимости фабрики
        },
      ],
      exports: ['CONFIG_OPTIONS'],
    };
  }
  // Обьявление модуля для локального инстанцирования
  static forFeature(): DynamicModule {
    return {
      module: ConfigModule,
      providers: [
        ConfigService,
        {
          provide: 'CONFIG_OPTIONS',
          useFactory: (configService: ConfigService) =>
            configService.getOptions(),
          inject: [ConfigService], // Описываем зависимости фабрики
        },
      ],
      exports: ['CONFIG_OPTIONS'],
    };
  }
}

Plugins to expand functionality

One of the important features Nexus-IoC is the ability to expand functionality using plugins. They allow you to add new features without changing the core library code.

One example is integration with a tool for analyzing and visualizing a dependency graph.

To do this, Nexus-IoC provides a method addScannerPluginwith which you can connect plugins at the stage of scanning the dependency graph. This method allows the integration of third-party tools that can interact with the graph during its construction.

How addScannerPlugin works

Method addScannerPlugin accepts the plugin as a function that will be called after the dependency graph construction stage. The plugin receives information about the graph, its nodes and edges. It is possible to implement additional check or modify the graph.

The first plugin that was created is GraphScannerVisualizer . Its task is to visualize the graph.

import { NexusApplicationsServer } from '@nexus-ioc/core/dist/server';
import { GraphScannerVisualizer } from 'nexus-ioc-graph-visualizer';
import { AppModule } from './apps'; 

// Добавляем плагин визуализации
const visualizer = new GraphScannerVisualizer("./graph.png");

async function bootstrap() {
	await NexusApplicationsServer.create(AppModule)
		.addScannerPlugin(visualizer)
		.bootstrap();
}

bootstrap();

Comparison with other options

Example on Nexus-IoC
import { Injectable, NsModule, Scope } from '@nexus-ioc/core';
import { NexusApplicationsServer } from '@nexus-ioc/core/dist/server'; 

@Injectable({ scope: Scope.Singleton })
class LoggerService {
  log(message: string) {
    console.log(message);
  }
}

@Injectable()
class UserService {
  constructor(private logger: LoggerService) {}

  printUser(userId: string) {
    this.logger.log(`logger: ${userId}`);
  }
}

@NsModule({
  providers: [LoggerService, UserService],
})
class AppModule {}

async function bootstrap() {
  const container = new NexusApplicationsServe.create(AppModule).bootstrap();
  const userService = await container.get<UserService>(UserService);
  
  userService.printUser('log me!');
}

bootstrap();
example on inversify
import 'reflect-metadata';
import { Container, injectable, inject } from 'inversify';

@injectable()
class LoggerService {
  log(message: string) {
    console.log(message);
  }
}

@injectable()
class UserService {
  constructor(@inject(LoggerService) private logger: LoggerService) {}

  printUser(userId: string) {
    this.logger.log(`User ID: ${userId}`);
  }
}

const container = new Container();
container.bind(LoggerService).toSelf();
container.bind(UserService).toSelf();

const userService = container.get(UserService);
userService.printUser('123');
example on Tsyringe:
import 'reflect-metadata';
import { container, injectable } from 'tsyringe';

@injectable()
class LoggerService {
  log(message: string) {
    console.log(message);
  }
}

@injectable()
class UserService {
  constructor(private logger: LoggerService) {}

  printUser(userId: string) {
    this.logger.log(`User ID: ${userId}`);
  }
}

container.registerSingleton(LoggerService);
container.registerSingleton(UserService);

const userService = container.resolve(UserService);
userService.printUser('123');

As you can see, there is no wunderwaffle here that would change the rules of the game and destroy competitors, the library manages dependencies, just doing it a little differently. The main difference from other solutions is that the declarative declaration of modules opens up great opportunities for static code analysiswhich helps during development large applications.

Finally

Who will benefit from this solution: Nexus-IoC is particularly well suited for large applications (enterprise level), where not only dependency management is important, but also clarity of application structure. I would not recommend this solution for small and medium-sized applications – here you can easily do without DI, especially in the initial stages. However, when the project becomes large-scale, with dozens of developers and teams interacting through contracts, Nexus-IoC can alleviate many of the problems associated with dependency management while providing powerful tools for code maintenance and analysis.

Plans:

  • The API is already stable and will not changebut there is still work to be done on optimization and full test coverage to bring the library to version 1.0

  • Development CLI to simplify working with the library

  • Creation static dependency graph analyzerto identify errors before the assembly stage

  • Development plugins for IDE to improve integration with editors

  • Improved documentation and site creation for the convenience of developers

Repository link: https://github.com/Isqanderm/ioc

Link to npm packages: https://www.npmjs.com/settings/nexus-ioc/packages

Github Wiki: https://github.com/Isqanderm/ioc/wiki

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *