Gathering detailed insights and metrics for @apiratorjs/di-container
Gathering detailed insights and metrics for @apiratorjs/di-container
Gathering detailed insights and metrics for @apiratorjs/di-container
Gathering detailed insights and metrics for @apiratorjs/di-container
npm install @apiratorjs/di-container
Typescript
Module System
Min. Node Version
Node Version
NPM Version
TypeScript (100%)
Total Downloads
1,704
Last Day
10
Last Week
42
Last Month
729
Last Year
1,704
MIT License
2 Stars
28 Commits
1 Watchers
1 Branches
1 Contributors
Updated on May 02, 2025
Minified
Minified + Gzipped
Latest Version
1.4.1
Package Id
@apiratorjs/di-container@1.4.1
Unpacked Size
54.89 kB
Size
13.85 kB
File Count
31
NPM Version
10.8.2
Node Version
20.19.0
Published on
May 02, 2025
Cumulative downloads
Total Downloads
Last Day
100%
10
Compared to previous day
Last Week
-78.7%
42
Compared to previous week
Last Month
183.7%
729
Compared to previous month
Last Year
0%
1,704
Compared to previous year
2
3
A lightweight dependency injection container for JavaScript and TypeScript with powerful features: modular organization with DiModule.create, lazy initialization, automatic circular dependency detection, and multiple service lifecycles (singleton with both in-place and lazy initialization, request-scoped, transient). Includes built-in async context management, lifecycle hooks (onConstruct/onDispose), and remains completely framework-agnostic for flexible application architecture.
Note: Requires Node.js version >=16.4.0
Multiple Lifecycles:
Lazy Initialization: Services are only created when requested.
Async Context Management: Leverages @apiratorjs/async-context to manage request scopes.
Circular Dependency Detection: Automatically detects and reports circular dependencies with detailed chain information through CircularDependencyError
.
Lifecycle Hooks: Services can implement onConstruct() and onDispose() for custom initialization and cleanup.
Concurrency Safety: Designed to avoid race conditions during lazy instantiation.
Modular Organization: Services can be organized into modules, allowing for better separation of concerns and reusability.
Install via npm:
1npm install @apiratorjs/di-container
Or using yarn:
1yarn add @apiratorjs/di-container
Create and configure your DI container using the DiConfigurator, then build a DiContainer for runtime service resolution.
Any service is registered with a factory function that returns a promise of the service instance. Tokens are used to identify services. Tokens can be strings, symbols, or classes.
1import { DiConfigurator } from "@apiratorjs/di-container"; 2import { AsyncContextStore } from "@apiratorjs/async-context"; 3 4// Create a configurator instance. 5const diConfigurator = new DiConfigurator(); 6 7// Register a singleton service (default: in-place initialization during container build). 8diConfigurator.addSingleton("MY_SINGLETON", async () => { 9 return new MySingletonService(); 10}); 11 12// Register a singleton service with lazy initialization (created only when requested). 13diConfigurator.addSingleton("MY_LAZY_SINGLETON", async () => { 14 return new MySingletonService(); 15}, { isLazy: true }); 16 17// Register a request-scoped service. 18diConfigurator.addScoped("MY_SCOPED", async () => { 19 return new MyScopedService(); 20}); 21 22// Register a transient service. 23diConfigurator.addTransient("MY_TRANSIENT", async () => { 24 return new MyTransientService(); 25}); 26 27// Build the container. 28const diContainer = diConfigurator.build();
You can resolve services directly via the configurator or through the built container:
1// Resolve a singleton service. 2const singletonInstance = await diConfigurator.resolve("MY_SINGLETON"); 3 4// Resolve a transient service. 5const transientInstance = await diConfigurator.resolve("MY_TRANSIENT");
For services registered as scoped, you must resolve them within a request scope:
1import { AsyncContextStore } from "@apiratorjs/async-context"; 2 3await diContainer.runWithNewRequestScope(new AsyncContextStore(), async () => { 4 const scopedInstance = await diContainer.resolve("MY_SCOPED"); 5 console.log(scopedInstance); 6});
[!WARNING] You cannot resolve a request-scoped service outside of a request scope.
If a service implements the optional lifecycle hooks (onConstruct and/or onDispose), they are invoked automatically when the service is created and disposed.
1diConfigurator.addSingleton("HOOKED_SINGLETON", async () => { 2 return { 3 async onConstruct() { 4 console.log("Service constructed!"); 5 }, 6 async onDispose() { 7 console.log("Service disposed!"); 8 } 9 }; 10});
When the service is resolved for the first time, onConstruct() is called; later, when the container disposes of the service, onDispose() is invoked.
When registering multiple services with the same token, only the last registered implementation will be used:
1// This implementation will be overridden 2diConfigurator.addSingleton("MY_SERVICE", async () => { 3 return new FirstImplementation(); 4}); 5 6// This implementation will be used when resolving MY_SERVICE 7diConfigurator.addSingleton("MY_SERVICE", async () => { 8 return new SecondImplementation(); 9}); 10 11// This works the same way for all service types (singleton, scoped, transient)
This behavior can be useful for overriding services in testing scenarios or when customizing default implementations.
The container automatically detects circular dependencies during service resolution and throws a CircularDependencyError
with detailed information about the dependency chain:
1// This would create a circular dependency 2diConfigurator.addSingleton("ServiceA", async (di) => { 3 await di.resolve("ServiceB"); 4 return new ServiceA(); 5}); 6 7diConfigurator.addSingleton("ServiceB", async (di) => { 8 await di.resolve("ServiceA"); 9 return new ServiceB(); 10}); 11 12// This will throw CircularDependencyError 13try { 14 await diConfigurator.resolve("ServiceA"); 15} catch (error) { 16 if (error instanceof CircularDependencyError) { 17 // error.chain contains the full dependency chain: ["ServiceA", "ServiceB", "ServiceA"] 18 console.error(`Circular dependency detected: ${error.chain.join(" -> ")}`); 19 } 20}
The error provides a complete dependency chain for debugging purposes, making it easier to identify and fix circular dependencies in your application.
1import { DiConfigurator } from "../src"; 2import { IOnConstruct, IOnDispose } from "../src/types"; 3import { AsyncContextStore } from "@apiratorjs/async-context"; 4 5class User { 6 public constructor( 7 public readonly email: string, 8 public readonly age: number 9 ) {} 10} 11 12class Config { 13 public dbProvider = "in_memory"; 14} 15 16// Emulate a db storage 17const users: User[] = []; 18 19class DBContext implements IOnConstruct, IOnDispose { 20 public constructor(private readonly _config: Config) {} 21 22 onDispose(): Promise<void> | void { 23 console.log("DBContext disposed"); 24 } 25 26 onConstruct(): Promise<void> | void { 27 console.log("DBContext constructed. Provider: ", this._config.dbProvider); 28 } 29 30 findUserByEmail(email: string): User | undefined { 31 return users.find(user => user.email === email); 32 } 33 34 addUser(user: User): void { 35 users.push(user); 36 } 37} 38 39class UserService { 40 public constructor(private readonly _db: DBContext) {} 41 42 public getUserByEmail(email: string): User | undefined { 43 return this._db.findUserByEmail(email); 44 } 45 46 public addUser(user: User): void { 47 this._db.addUser(user); 48 } 49} 50 51const diConfigurator = new DiConfigurator(); 52 53diConfigurator.addSingleton(Config, () => new Config()); 54diConfigurator.addScoped(DBContext, async (cfg) => { 55 const config = await cfg.resolve(Config); 56 return new DBContext(config); 57}); 58diConfigurator.addScoped(UserService, async (cfg) => { 59 const dbContext = await cfg.resolve(DBContext); 60 return new UserService(dbContext); 61}); 62 63const diContainer = diConfigurator.build(); 64 65(async () => { 66 // To use request-scoped services, you need to create a new scope 67 await diContainer.runWithNewRequestScope(new AsyncContextStore(), async () => { 68 const userService = await diContainer.resolve(UserService); 69 70 userService.addUser(new User("john@doe.com", 30)); 71 }); 72 73 const user = await diContainer.runWithNewRequestScope(new AsyncContextStore(), async () => { 74 const userService = await diContainer.resolve(UserService); 75 76 return userService.getUserByEmail("john@doe.com"); 77 }); 78 79 console.log("User: ", user); 80})(); 81 82/** 83 * Output: 84 * 85 * DBContext constructed. Provider: in_memory 86 * DBContext disposed 87 * DBContext constructed. Provider: in_memory 88 * DBContext disposed 89 * User: User { email: 'john@doe.com', age: 30 } 90 */
The DI container supports organizing your dependencies into logical modules, making it easier to manage complex applications with many services. Modules provide a way to group related services together and can be reused across different parts of your application.
A module is a class or object that defines a set of related services:
1import { DiConfigurator } from "@apiratorjs/di-container"; 2import { IDiModule } from "./types"; 3 4// Define a module for database-related services 5class DatabaseModule implements IDiModule { 6 public register(configurator: DiConfigurator): void { 7 // Register database-related services 8 configurator.addSingleton("DATABASE_CONNECTION", async () => { 9 return new DatabaseConnection(/* connection params */); 10 }); 11 12 configurator.addScoped("TRANSACTION_MANAGER", async (di) => { 13 const connection = await di.resolve("DATABASE_CONNECTION"); 14 return new TransactionManager(connection); 15 }); 16 } 17} 18 19// Define a module for user-related services 20class UserModule implements IDiModule { 21 public register(configurator: DiConfigurator): void { 22 // Register user-related services 23 configurator.addScoped("USER_REPOSITORY", async (di) => { 24 const transactionManager = await di.resolve("TRANSACTION_MANAGER"); 25 return new UserRepository(transactionManager); 26 }); 27 28 configurator.addScoped("USER_SERVICE", async (di) => { 29 const userRepository = await di.resolve("USER_REPOSITORY"); 30 return new UserService(userRepository); 31 }); 32 } 33}
For a more declarative approach, you can use the DiModule.create
static method to create modules with a configuration object:
1import { DiModule } from "@apiratorjs/di-container"; 2 3// Define service tokens 4const DATABASE_CONNECTION = Symbol("DATABASE_CONNECTION"); 5const LAZY_DATABASE_CONNECTION = Symbol("LAZY_DATABASE_CONNECTION"); 6const TRANSACTION_MANAGER = Symbol("TRANSACTION_MANAGER"); 7const USER_REPOSITORY = Symbol("USER_REPOSITORY"); 8const USER_SERVICE = Symbol("USER_SERVICE"); 9 10// Create a database module 11const DatabaseModule = DiModule.create({ 12 providers: [ 13 { 14 token: DATABASE_CONNECTION, 15 useFactory: async () => { 16 return new DatabaseConnection(/* connection params */); 17 }, 18 lifetime: "singleton" 19 // By default, created during container build 20 }, 21 { 22 token: LAZY_DATABASE_CONNECTION, 23 useFactory: async () => { 24 return new DatabaseConnection(/* connection params */); 25 }, 26 lifetime: "singleton", 27 options: { isLazy: true } // Will be created only when first requested 28 }, 29 { 30 token: TRANSACTION_MANAGER, 31 useFactory: async (di) => { 32 const connection = await di.resolve(DATABASE_CONNECTION); 33 return new TransactionManager(connection); 34 }, 35 lifetime: "scoped" 36 } 37 ] 38}); 39 40// Create a user module that imports the database module 41const UserModule = DiModule.create({ 42 imports: [DatabaseModule], // Import other modules 43 providers: [ 44 { 45 token: USER_REPOSITORY, 46 useFactory: async (di) => { 47 const txManager = await di.resolve(TRANSACTION_MANAGER); 48 return new UserRepository(txManager); 49 }, 50 lifetime: "scoped" 51 }, 52 { 53 token: USER_SERVICE, 54 useFactory: async (di) => { 55 const userRepo = await di.resolve(USER_REPOSITORY); 56 return new UserService(userRepo); 57 }, 58 lifetime: "scoped" 59 } 60 ] 61});
The DiModule.create
method accepts a ModuleOptions
object with the following properties:
imports
: An array of other modules to importproviders
: An array of service provider configurations, each with:
token
: The service token (string, symbol, or class)useFactory
: A factory function that creates the servicelifetime
: The service lifetime ("singleton", "scoped", or "transient")options
: Additional options specific to the lifetime:
{ isLazy: true }
- When true, the singleton will be created only when requested, not during DI build stepThis declarative approach makes it easy to organize your services and their dependencies, and enables importing modules into other modules.
You can register modules with your DiConfigurator using the addModule
method:
1const diConfigurator = new DiConfigurator(); 2 3// Register a custom module class 4const databaseModule = new DatabaseModule(); 5diConfigurator.addModule(databaseModule); 6 7// Register a module created with DiModule.create 8const userModule = DiModule.create({ 9 // module options 10}); 11diConfigurator.addModule(userModule); 12 13// Build the container 14const diContainer = diConfigurator.build();
For larger applications, you can organize your modules in a hierarchical structure:
1// Create the core modules 2const coreModule = DiModule.create({ 3 providers: [/* core services */] 4}); 5 6const dataModule = DiModule.create({ 7 imports: [coreModule], 8 providers: [/* data services */] 9}); 10 11// Create feature modules that depend on core and data modules 12const featureModule = DiModule.create({ 13 imports: [coreModule, dataModule], 14 providers: [/* feature-specific services */] 15}); 16 17// Create the root application module 18const appModule = DiModule.create({ 19 imports: [ 20 coreModule, 21 dataModule, 22 featureModule, 23 // other feature modules 24 ], 25 providers: [/* app-specific services */] 26}); 27 28// Register only the root module 29const diConfigurator = new DiConfigurator(); 30diConfigurator.addModule(appModule);
When registering multiple modules, be aware that:
This allows for service overrides and customization at different levels of your module hierarchy.
Here's a comprehensive example that demonstrates how to use DiModule.create to organize a complete application with multiple modules and dependencies:
1import { DiConfigurator, DiModule } from "@apiratorjs/di-container"; 2 3// Define interfaces 4interface ILogger { 5 log(message: string): void; 6} 7 8interface IAuthService { 9 isAuthenticated(): boolean; 10} 11 12interface IUserService { 13 getCurrentUser(): string; 14} 15 16// Implement services 17class ConsoleLogger implements ILogger { 18 log(message: string): void { 19 console.log(`[LOG] ${message}`); 20 } 21} 22 23class AuthServiceImpl implements IAuthService { 24 constructor(private logger: ILogger) {} 25 26 isAuthenticated(): boolean { 27 this.logger.log("Checking authentication"); 28 return true; 29 } 30} 31 32class UserServiceImpl implements IUserService { 33 constructor(private logger: ILogger, private authService: IAuthService) {} 34 35 getCurrentUser(): string { 36 this.logger.log("Getting current user"); 37 return this.authService.isAuthenticated() ? "John Doe" : "Guest"; 38 } 39} 40 41// Define service tokens 42const LOGGER = Symbol("LOGGER"); 43const AUTH_SERVICE = Symbol("AUTH_SERVICE"); 44const USER_SERVICE = Symbol("USER_SERVICE"); 45 46// Create modules 47const LoggingModule = DiModule.create({ 48 providers: [ 49 { 50 token: LOGGER, 51 useFactory: () => new ConsoleLogger(), 52 lifetime: "singleton" 53 } 54 ] 55}); 56 57const AuthModule = DiModule.create({ 58 imports: [LoggingModule], // Import logging module 59 providers: [ 60 { 61 token: AUTH_SERVICE, 62 useFactory: async (cfg: DiConfigurator) => { 63 const logger = await cfg.resolve<ILogger>(LOGGER); 64 return new AuthServiceImpl(logger); 65 }, 66 lifetime: "singleton" 67 } 68 ] 69}); 70 71const UserModule = DiModule.create({ 72 imports: [LoggingModule, AuthModule], // Import both modules 73 providers: [ 74 { 75 token: USER_SERVICE, 76 useFactory: async (cfg: DiConfigurator) => { 77 const logger = await cfg.resolve<ILogger>(LOGGER); 78 const authService = await cfg.resolve<IAuthService>(AUTH_SERVICE); 79 return new UserServiceImpl(logger, authService); 80 }, 81 lifetime: "singleton" 82 } 83 ] 84}); 85 86// Create the application module that imports all other modules 87const AppModule = DiModule.create({ 88 imports: [UserModule], 89 providers: [ 90 // You can add app-specific services here 91 ] 92}); 93 94// Usage 95async function main() { 96 const configurator = new DiConfigurator(); 97 98 // Register the top-level module 99 configurator.addModule(AppModule); 100 101 // Build the container 102 const container = configurator.build(); 103 104 // Resolve and use a service 105 const userService = await container.resolve<IUserService>(USER_SERVICE); 106 const currentUser = userService.getCurrentUser(); 107 console.log(`Current user: ${currentUser}`); 108 109 // Clean up 110 await container.dispose(); 111} 112 113main().catch(console.error);
Output:
[LOG] Checking authentication
[LOG] Getting current user
Current user: John Doe
This example demonstrates:
Contributions, issues, and feature requests are welcome! Please open an issue or submit a pull request on GitHub.
No vulnerabilities found.
No security vulnerabilities found.