@apiratorjs/di-container
Version:
A lightweight dependency injection container for JavaScript and TypeScript with powerful features: modular organization, service discovery for runtime introspection, service tagging for multiple implementations, lazy initialization, automatic circular dep
828 lines (626 loc) • 32 kB
Markdown
# /di-container
[](https://www.npmjs.com/package/@apiratorjs/di-container)
[](https://github.com/apiratorjs/di-container/blob/main/LICENSE)
A lightweight dependency injection container for JavaScript and TypeScript with powerful features: modular organization, service discovery for runtime introspection, service tagging for multiple implementations, 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**
---
## Features
- **Multiple Lifecycles:**
- **Singleton:** One instance per application. By default lazily initialized (created only when requested), but can be configured for eager initialization during DI build step.
- **Request-Scoped:** One instance per request scope using asynchronous context (lazily loaded).
- **Transient:** A new instance on every resolution.
- **Service Tags:** Support for registering multiple implementations of the same service token using tags, enabling flexible service resolution based on context.
- **Lazy Initialization:** Services are only created when requested (default for singletons, but can be overridden with eager initialization).
- **Registration Order**: Last registration wins for same token+tag combinations
- **Async Context Management:** Leverages [/async-context](https://github.com/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.
- **Singleton:** Supports both onConstruct() and onDispose() hooks.
- **Request-Scoped:** Supports both onConstruct() and onDispose() hooks.
- **Transient:** Supports only onConstruct() hooks.
- **Concurrency Safety:** Designed to avoid race conditions during lazy instantiation.
- **Service Discovery:** Built-in discovery service for introspecting registered services by token, lifetime, tag, or getting all registrations.
- **Modular Organization:** Services can be organized into modules, allowing for better separation of concerns and reusability.
---
## Installation
Install via npm:
```bash
npm install /di-container
```
Or using yarn:
```bash
yarn add /di-container
```
## Usage
### Quick Start
Create and configure your DI container with the DiConfigurator, then build a DiContainer for runtime usage:
```typescript
import { DiConfigurator } from "@apiratorjs/di-container";
import { AsyncContextStore } from "@apiratorjs/async-context";
// Service classes with lifecycle hooks
class DatabaseService {
async onConstruct() {
console.log("Database connected");
}
async onDispose() {
console.log("Database disconnected");
}
async query(sql: string) {
return `Result for: ${sql}`;
}
}
class UserService {
constructor(private db: DatabaseService) {}
async getUser(id: string) {
return await this.db.query(`SELECT * FROM users WHERE id = ${id}`);
}
}
// Configure services with different lifecycles and options
const configurator = new DiConfigurator();
// Singleton with eager initialization
configurator.addSingleton("DATABASE", () => new DatabaseService(), {
eager: true,
});
// Scoped service with dependency injection
configurator.addScoped("USER_SERVICE", async (container) => {
const db = await container.resolve("DATABASE");
return new UserService(db);
});
// Transient service
configurator.addTransient("LOGGER", () => ({
log: (msg) => console.log(`[LOG] ${msg}`),
}));
// Build container (eagerly initializes singletons)
const container = await configurator.build();
// Use services - scoped services REQUIRE a request scope
await container.runWithNewRequestScope(async (container) => {
const userService = await container.resolve("USER_SERVICE"); // ✅ Works in scope
const logger = await container.resolve("LOGGER");
const user = await userService.getUser("123");
logger.log(`Retrieved user: ${user}`);
}, new AsyncContextStore());
// ❌ This would throw RequestScopeResolutionError:
// await container.resolve("USER_SERVICE"); // Error: scoped service outside scope
// Cleanup when done
await container.dispose();
```
### Core Concepts
**Service Lifecycles:**
- **Singleton**: One instance per application (lazy by default, can be eager)
- **Scoped**: One instance per request scope - **MUST** be used within `runWithNewRequestScope()`, throws `RequestScopeResolutionError` otherwise
- **Transient**: New instance on every resolution
**Important:** Scoped services cannot be resolved outside a request scope:
```typescript
// ✅ Correct usage
await container.runWithNewRequestScope(async (container) => {
const scopedService = await container.resolve("SCOPED_SERVICE"); // Works
}, new AsyncContextStore());
// ❌ This throws RequestScopeResolutionError
const scopedService = await container.resolve("SCOPED_SERVICE"); // Error!
```
**Service Tags (Optional):** Register multiple implementations when needed. If no tag is specified, services automatically get the "default" tag:
```typescript
configurator.addSingleton("PAYMENT", () => new StripePayment(), {
tag: "stripe",
});
configurator.addSingleton("PAYMENT", () => new PayPalPayment(), {
tag: "paypal",
});
configurator.addSingleton("PAYMENT", () => new BankPayment()); // Gets "default" tag automatically
```
**Registration Strategy:** When registering a service with the same token and tag combination multiple times, the **last registration wins**. This allows for service overriding and configuration flexibility:
```typescript
// First registration
configurator.addSingleton("DATABASE", () => new PostgresDatabase());
// This will override the previous registration
configurator.addSingleton("DATABASE", () => new MySQLDatabase()); // MySQL wins
// Different tags don't override each other
configurator.addSingleton("DATABASE", () => new MongoDatabase(), {
tag: "nosql",
}); // Separate service
```
**Lifecycle Hooks:** Services can implement `onConstruct()` and `onDispose()` for automatic initialization and cleanup.
## IDiConfigurator Interface
The `IDiConfigurator` is the main interface for configuring dependency injection services. Here are all available methods:
### Service Registration Methods
| Method | Description | Example |
| ------------------------------------------------- | ------------------------------------------------------------------------- | ----------------------------------------------------------------------------- |
| `addSingleton<T>(token, factory, options?): this` | Register a singleton service (tag in options, defaults to "default") | `configurator.addSingleton("DB", () => new Database(), { eager: true })` |
| `addScoped<T>(token, factory, options?): this` | Register a request-scoped service (tag in options, defaults to "default") | `configurator.addScoped("USER_CTX", async (container) => new UserContext())` |
| `addTransient<T>(token, factory, options?): this` | Register a transient service (tag in options, defaults to "default") | `configurator.addTransient("LOGGER", () => new Logger(), { tag: "console" })` |
| `addModule(module): this` | Register a module (calls module.register(configurator)) | `configurator.addModule(new DatabaseModule())` |
#### Options Interfaces
**ISingletonServiceRegistrationOptions** (for `addSingleton`):
```typescript
interface ISingletonServiceRegistrationOptions {
tag?: string; // Service tag (defaults to "default")
eager?: boolean; // Should the singleton be eagerly created during container build
}
```
**IScopedServiceRegistrationOptions** (for `addScoped`):
```typescript
interface IScopedServiceRegistrationOptions {
tag?: string; // Service tag (defaults to "default")
}
```
**ITransientServiceRegistrationOptions** (for `addTransient`):
```typescript
interface ITransientServiceRegistrationOptions {
tag?: string; // Service tag (defaults to "default")
}
```
### Container Management Methods
| Method | Description | Returns |
| --------------------------------------------- | --------------------------- | ------------------------------------------------------------------------------ |
| `build<T extends IBuildOptions>(options?: T)` | Build the runtime container | `Promise<T extends { autoInit: false } ? IInitableDiContainer : IDiContainer>` |
| `getDiscoveryService()` | Get discovery service | `DiDiscoveryService` for service introspection |
### Practical Example
```typescript
const configurator = new DiConfigurator();
// Registration - various lifecycles and features
configurator
.addSingleton("CONFIG", () => ({ env: "prod" }), { eager: true })
.addScoped("REQUEST_ID", () => Math.random().toString(36))
.addTransient("LOGGER", () => new ConsoleLogger());
// Build container for runtime usage
const container = await configurator.build();
// Service introspection during configuration
const discovery = configurator.getDiscoveryService();
const eagerServices = discovery
.getServicesByLifetime("singleton")
.filter((s) => s.singletonOptions?.eager);
console.log("Eager services configured:", eagerServices.length);
// ✅ Use the container for service resolution:
await container.runWithNewRequestScope(async (container) => {
const requestId = await container.resolve("REQUEST_ID"); // Works in scope
const logger = await container.resolve("LOGGER");
console.log(`Processing request: ${requestId}`);
}, new AsyncContextStore());
await container.dispose();
```
## IDiContainer Interface
The `IDiContainer` is the runtime interface for resolving services after building your DI configuration. It provides a clean, read-only interface focused on service resolution and request scope management.
### Service Resolution Methods
| Method | Description | Returns |
| --------------------------------- | ---------------------------------------------- | --------------------------------- |
| `resolve<T>(token, tag?)` | Resolve a service (optional) | `Promise<T \| undefined>` |
| `resolveRequired<T>(token, tag?)` | Resolve a service (throws if not found) | `Promise<T>` |
| `resolveAll<T>(token)` | Resolve all implementations with metadata | `Promise<IResolveAllResult<T>[]>` |
| `resolveAllRequired<T>(token)` | Resolve all implementations (throws if none found) | `Promise<IResolveAllResult<T>[]>` |
| `resolveTagged<T>(tag)` | Resolve first service with tag | `Promise<T \| undefined>` |
| `resolveTaggedRequired<T>(tag)` | Resolve service with tag (throws if not found) | `Promise<T>` |
| `resolveAllTagged(tag)` | Resolve all services with tag and metadata | `Promise<IResolveAllResult[]>` |
| `resolveAllTaggedRequired(tag)` | Resolve all services with tag (throws if none found) | `Promise<IResolveAllResult[]>` |
#### Optional vs Required Resolution
The DI container provides two variants for each resolution method:
**Optional Methods** (`resolve`, `resolveAll`, `resolveTagged`, `resolveAllTagged`):
- Return `undefined` or empty array when no services are found
- Suitable for optional dependencies or graceful degradation scenarios
- No exceptions thrown for missing services
**Required Methods** (`resolveRequired`, `resolveAllRequired`, `resolveTaggedRequired`, `resolveAllTaggedRequired`):
- `resolveRequired` and `resolveAllRequired` throw `UnregisteredDependencyError` when no services are found
- `resolveTaggedRequired` and `resolveAllTaggedRequired` throw `UnregisteredTagError` when no services with the specified tag are found
- Suitable for critical dependencies that must exist
- Fail-fast approach for better error detection
```typescript
// Optional resolution - graceful handling
const optionalLogger = await container.resolve("LOGGER");
if (optionalLogger) {
optionalLogger.log("Service available");
} else {
console.log("Logger not available, using console");
}
// Required resolution - fail-fast
try {
const requiredDatabase = await container.resolveRequired("DATABASE");
// Guaranteed to have database instance here
await requiredDatabase.connect();
} catch (error) {
console.error("Critical dependency missing:", error.message);
process.exit(1);
}
// Optional multiple resolution
const optionalPayments = await container.resolveAll("PAYMENT");
console.log(`Found ${optionalPayments.length} payment processors`); // Could be 0
// Required multiple resolution
try {
const requiredPayments = await container.resolveAllRequired("PAYMENT");
console.log(`Using ${requiredPayments.length} payment processors`); // At least 1
} catch (error) {
console.error("No payment processors available!"); // Critical error
}
```
### Runtime Management Methods
| Method | Description | Returns |
| ------------------------------------------------ | ---------------------------------------------------------------- | -------------------- |
| `runWithNewRequestScope(callback, initialStore)` | Execute code in request scope (**Required** for scoped services) | `Promise<void>` |
| `isInRequestScopeContext()` | Check if in request scope | `boolean` |
| `dispose()` | Dispose all services (cleanup singletons and scoped services) | `Promise<void>` |
| `getDiscoveryService()` | Get discovery service for introspection | `DiDiscoveryService` |
### Key Differences from IDiConfigurator
**IDiContainer is for runtime usage only:**
- ✅ **Service Resolution**: All resolve methods available
- ✅ **Request Scope Management**: Required for scoped services
- ✅ **Discovery**: Service introspection and health checks
- ✅ **Cleanup**: Proper disposal of resources
- ❌ **No Service Registration**: Cannot add new services
- ❌ **No Building**: Already built and ready to use
### Practical Usage Example
```typescript
const configurator = new DiConfigurator();
configurator
.addSingleton("DATABASE", () => new DatabaseService(), { eager: true })
.addScoped("USER_CTX", () => ({ userId: "user123" }))
.addTransient("LOGGER", () => new ConsoleLogger());
// Build creates the runtime container
const container = await configurator.build();
// ✅ Singleton and transient services work anywhere
const logger = await container.resolve("LOGGER");
logger?.log("Application started");
// ✅ Scoped services MUST be used within request scope
await container.runWithNewRequestScope(async (container) => {
const userCtx = await container.resolveRequired("USER_CTX"); // Works in scope
const database = await container.resolve("DATABASE");
console.log(`Processing for user: ${userCtx.userId}`);
}, new AsyncContextStore());
// ❌ This throws RequestScopeResolutionError:
// const userCtx = await container.resolve("USER_CTX"); // Error!
// Health check using discovery
const discovery = container.getDiscoveryService();
const eagerSingletons = discovery
.getServicesByLifetime("singleton")
.filter((s) => s.singletonOptions?.eager);
console.log(
"Eager services initialized:",
eagerSingletons.every((s) => s.isResolved)
);
// Cleanup when shutting down
await container.dispose();
```
### Service Discovery
Query and introspect registered services for debugging, monitoring, and dynamic resolution. The discovery service returns `IServiceRegistration` objects with detailed information about each service.
#### IResolveAllResult Interface
The `resolveAll` and `resolveAllTagged` methods return results with both the service instance and its registration metadata:
| Property | Type | Description |
| -------------- | ------------------------- | ----------------------------- |
| `instance` | `T` | The resolved service instance |
| `registration` | `IServiceRegistration<T>` | Service registration metadata |
Example usage:
```typescript
const results = await container.resolveAll<UserService>("USER_SERVICE");
results.forEach((result) => {
console.log("Service instance:", result.instance);
console.log("Service tag:", result.registration.tag);
console.log("Service lifetime:", result.registration.lifetime);
console.log("Is resolved:", result.registration.isResolved);
});
// Extract just the instances if you only need them
const instances = results.map((result) => result.instance);
```
#### IServiceRegistration Interface
Each service registration returned by the discovery service contains:
| Property | Type | Description |
| ------------------ | ---------------------------------------- | --------------------------------------------------------------- |
| `token` | `TServiceToken` | The service token (string, symbol, or class) |
| `tokenType` | `"string" \| "symbol" \| "class"` | Type of the token |
| `lifetime` | `"singleton" \| "scoped" \| "transient"` | Service lifetime |
| `tag` | `string` | Service tag (automatically set to "default" when not specified) |
| `isResolved` | `boolean` | Whether service instance has been created |
| `singletonOptions` | `ISingletonOptions?` | Options for singleton services |
| `metatype` | `TClassType?` | Class constructor if token is a class |
#### Discovery Service Methods
| Method | Description | Returns |
| ---------------------------------- | --------------------------- | ------------------------ |
| `getAll()` | Get all registered services | `IServiceRegistration[]` |
| `getServicesByTag(tag)` | Get services by tag | `IServiceRegistration[]` |
| `getServicesByServiceToken(token)` | Get services by token | `IServiceRegistration[]` |
| `getServicesByLifetime(lifetime)` | Get services by lifetime | `IServiceRegistration[]` |
#### IServiceRegistration Methods
| Method | Description |
| --------------- | ---------------------------------------------- |
| `getInstance()` | Get the current service instance (if resolved) |
#### Discovery Example
```typescript
const configurator = new DiConfigurator();
// Register services
configurator.addSingleton("DATABASE", () => new DatabaseService(), {
eager: true,
});
configurator.addScoped("USER_SERVICE", () => new UserService());
configurator.addTransient("LOGGER", () => new LoggerService());
const discovery = configurator.getDiscoveryService();
// Query by different criteria
const allServices = discovery.getAll();
const singletons = discovery.getServicesByLifetime("singleton");
const databaseServices = discovery.getServicesByServiceToken("DATABASE");
// Work with service registrations
const databaseReg = databaseServices[0];
console.log(`Database service token: ${databaseReg.token}`);
console.log(`Token type: ${databaseReg.tokenType}`);
console.log(`Is eager: ${databaseReg.singletonOptions?.eager}`);
console.log(`Is resolved: ${databaseReg.isResolved}`);
// Health check for eager services
const eagerServices = discovery
.getServicesByLifetime("singleton")
.filter((s) => s.singletonOptions?.eager)
.map((s) => ({ token: s.token, resolved: s.isResolved }));
console.log("Eager services status:", eagerServices);
// Service inventory
console.table(
discovery.getAll().map((s) => ({
Token: s.token.toString(),
Type: s.tokenType,
Lifetime: s.lifetime,
Tag: s.tag,
Resolved: s.isResolved,
Eager: s.singletonOptions?.eager || false,
}))
);
```
### Advanced Features
**Circular Dependency Detection:** Automatic detection with detailed error chains:
```typescript
// This creates a circular dependency
configurator.addSingleton("ServiceA", async (container) => {
await container.resolve("ServiceB"); // Will detect the cycle
return new ServiceA();
});
configurator.addSingleton("ServiceB", async (container) => {
await container.resolve("ServiceA");
return new ServiceB();
});
// Throws CircularDependencyError with chain: ["ServiceA", "ServiceB", "ServiceA"]
```
**Complete Application Example:**
```typescript
import {
DiConfigurator,
IOnConstruct,
IOnDispose,
} from "@apiratorjs/di-container";
import { AsyncContextStore } from "@apiratorjs/async-context";
// Service classes
class Config {
public readonly dbUrl = "mongodb://localhost";
}
class Database implements IOnConstruct, IOnDispose {
constructor(private config: Config) {}
async onConstruct() {
console.log(`Connected to ${this.config.dbUrl}`);
}
async onDispose() {
console.log("Database disconnected");
}
async findUser(email: string) {
return { email, id: Math.random() };
}
}
class UserService {
constructor(private db: Database) {}
async getUser(email: string) {
return await this.db.findUser(email);
}
}
const configurator = new DiConfigurator();
configurator
.addSingleton("CONFIG", () => new Config(), { eager: true })
.addSingleton("DATABASE", async (container) => {
const config = await container.resolve("CONFIG");
return new Database(config);
})
.addScoped("USER_SERVICE", async (container) => {
const db = await container.resolve("DATABASE");
return new UserService(db);
});
// Usage - scoped services MUST be used within request scope
const container = await configurator.build();
await container.runWithNewRequestScope(async (container) => {
const userService = await container.resolve("USER_SERVICE"); // ✅ Works in scope
const user = await userService.getUser("user@example.com");
console.log("Found user:", user);
}, new AsyncContextStore());
// ❌ This would throw RequestScopeResolutionError:
// const userService = await container.resolve("USER_SERVICE"); // Error!
await container.dispose(); // Cleanup all services
```
### Advanced Disposal Management
The DI container provides granular control over service disposal with three disposal methods:
#### Complete Disposal: `dispose()`
Disposes all services (both singletons and scoped services in current request scope):
```typescript
const container = await configurator.build();
// Use services...
// Dispose everything when shutting down
await container.dispose(); // Calls both disposeSingletons() and disposeScopedServices()
```
#### Singleton-Only Disposal: `disposeSingletons()`
Disposes only singleton services and clears their instances. Useful for:
- Partial cleanup scenarios
- Restarting singleton services without affecting scoped services
- Memory management in long-running applications
```typescript
const container = await configurator.build();
// Use singleton services...
const config = await container.resolve("CONFIG");
const database = await container.resolve("DATABASE");
// Later, dispose only singletons (e.g., for hot reload or reconfiguration)
await container.disposeSingletons();
// Singletons are now disposed and will be recreated on next resolution
const newDatabase = await container.resolve("DATABASE"); // Creates new instance
```
#### Scoped-Only Disposal: `disposeScopedServices()`
Disposes only scoped services in the current request scope. Useful for:
- Manual cleanup before request scope ends
- Early resource release in long-running request scopes
- Custom request lifecycle management
```typescript
await container.runWithNewRequestScope(async (container) => {
// Use scoped services
const userContext = await container.resolve("USER_CONTEXT");
const sessionData = await container.resolve("SESSION_DATA");
// Perform some operations...
// Manually dispose scoped services before scope naturally ends
await container.disposeScopedServices();
// Scoped services are now disposed, but scope is still active
// Resolving them again will create new instances
const newUserContext = await container.resolve("USER_CONTEXT"); // Creates new instance
}, new AsyncContextStore());
```
#### Use Cases for Granular Disposal
**Hot Reload/Reconfiguration:**
```typescript
// Dispose and recreate only configuration-related singletons
await container.disposeSingletons();
// Next resolution will create fresh instances with new configuration
```
**Memory Management:**
```typescript
await container.runWithNewRequestScope(async (container) => {
// Process large dataset
const processor = await container.resolve("DATA_PROCESSOR");
await processor.processLargeDataset();
// Free memory early by disposing scoped services
await container.disposeScopedServices();
// Continue with lightweight operations...
}, new AsyncContextStore());
```
**Testing Scenarios:**
```typescript
// Clean up between test cases
await container.disposeSingletons(); // Reset all singleton state
```
### Working with Multiple Service Implementations
When you register multiple implementations of the same service with different tags, you can resolve them all at once:
```typescript
const configurator = new DiConfigurator();
// Register multiple payment processors
configurator.addSingleton("PAYMENT", () => new StripePayment(), {
tag: "stripe",
});
configurator.addSingleton("PAYMENT", () => new PayPalPayment(), {
tag: "paypal",
});
configurator.addSingleton("PAYMENT", () => new BankTransferPayment(), {
tag: "bank",
});
configurator.addSingleton("PAYMENT", () => new CashPayment()); // No tag specified = "default" tag
const container = await configurator.build();
// Resolve all payment implementations with metadata
const paymentResults = await container.resolveAll("PAYMENT");
console.log(`Found ${paymentResults.length} payment processors`); // Will show 4 processors
paymentResults.forEach((result) => {
console.log(`Payment processor: ${result.registration.tag}`);
console.log(`Instance:`, result.instance);
// Use the instance
const payment = result.instance;
payment.processPayment(100);
});
// Extract just the instances if you only need them
const paymentInstances = paymentResults.map((result) => result.instance);
// Or use resolveAllRequired to ensure at least one implementation exists
try {
const requiredPayments = await container.resolveAllRequired("PAYMENT");
console.log(`Guaranteed to have ${requiredPayments.length} payment processors`);
} catch (error) {
console.error("No payment processors registered!"); // Throws UnregisteredDependencyError
}
// Or resolve all services with a specific tag
const stripeResults = await container.resolveAllTagged("stripe");
console.log(`Found ${stripeResults.length} services with 'stripe' tag`);
// Use resolveAllTaggedRequired to ensure services with specific tag exist
try {
const requiredStripeServices = await container.resolveAllTaggedRequired("stripe");
console.log(`Guaranteed to have ${requiredStripeServices.length} stripe services`);
} catch (error) {
console.error("No services with 'stripe' tag found!"); // Throws UnregisteredTagError
}
// Resolve services with the default tag
const defaultResults = await container.resolveAllTagged("default");
console.log(`Found ${defaultResults.length} services with 'default' tag`);
// Or resolve the default payment processor directly
const defaultPayment = await container.resolveTagged("default"); // Gets CashPayment instance
await container.dispose();
```
## Modules
Organize related services into reusable modules for better code organization. Modules are simply a convenient way to group service registrations together.
```typescript
// Define service interfaces
interface ILogger {
log(message: string): void;
}
interface IUserService {
getCurrentUser(): string;
}
interface IAuthService {
isAuthenticated(): boolean;
}
// Service implementations
class ConsoleLogger implements ILogger {
log(message: string): void {
console.log(`[LOG] ${message}`);
}
}
class UserServiceImpl implements IUserService {
constructor(private logger: ILogger, private authService: IAuthService) {}
getCurrentUser(): string {
this.logger.log("Getting current user");
return this.authService.isAuthenticated() ? "John Doe" : "Guest";
}
}
class AuthServiceImpl implements IAuthService {
constructor(private logger: ILogger) {}
isAuthenticated(): boolean {
this.logger.log("Checking authentication");
return true;
}
}
// Define service tokens
const LOGGER = Symbol("LOGGER");
const USER_SERVICE = Symbol("USER_SERVICE");
const AUTH_SERVICE = Symbol("AUTH_SERVICE");
// Create modules to organize related services
class LoggingModule implements IDiModule {
register(configurator: DiConfigurator): void {
configurator.addSingleton(LOGGER, () => new ConsoleLogger());
}
}
class AuthModule implements IDiModule {
register(configurator: DiConfigurator): void {
configurator.addSingleton(AUTH_SERVICE, async (container) => {
const logger = await container.resolveRequired<ILogger>(LOGGER);
return new AuthServiceImpl(logger);
});
}
}
class UserModule implements IDiModule {
register(configurator: DiConfigurator): void {
configurator.addSingleton(USER_SERVICE, async (container) => {
const logger = await container.resolveRequired<ILogger>(LOGGER);
const authService = await container.resolveRequired<IAuthService>(
AUTH_SERVICE
);
return new UserServiceImpl(logger, authService);
});
}
}
// Register modules and use services
const configurator = new DiConfigurator();
configurator.addModule(new LoggingModule());
configurator.addModule(new AuthModule());
configurator.addModule(new UserModule());
const container = await configurator.build();
const userService = await container.resolveRequired<IUserService>(USER_SERVICE);
const currentUser = userService.getCurrentUser();
console.log(`Current user: ${currentUser}`);
await container.dispose();
```
**Module Features:**
- **Simple Organization**: Group related service registrations together
- **Reusability**: Modules can be reused across different applications
- **Clean Separation**: Separate concerns by domain or functionality
- **Standard Registration**: Use all standard service registration methods (addSingleton, addScoped, addTransient)
- **Dependency Injection**: Services in modules can depend on services from other modules
### Contributing
Contributions, issues, and feature requests are welcome!
Please open an issue or submit a pull request on [GitHub](https://github.com/apiratorjs/di-container).