npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@apiratorjs/di-container

v4.2.0

Published

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

Readme

@apiratorjs/di-container

NPM version License: MIT

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 @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:

npm install @apiratorjs/di-container

Or using yarn:

yarn add @apiratorjs/di-container

Usage

Quick Start

Create and configure your DI container with the DiConfigurator, then build a DiContainer for runtime usage:

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:

// ✅ 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:

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:

// 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):

interface ISingletonServiceRegistrationOptions {
  tag?: string; // Service tag (defaults to "default")
  eager?: boolean; // Should the singleton be eagerly created during container build
}

IScopedServiceRegistrationOptions (for addScoped):

interface IScopedServiceRegistrationOptions {
  tag?: string; // Service tag (defaults to "default")
}

ITransientServiceRegistrationOptions (for addTransient):

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

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
// 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

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:

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

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:

// 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:

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("[email protected]");
  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):

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
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
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:

// Dispose and recreate only configuration-related singletons
await container.disposeSingletons();
// Next resolution will create fresh instances with new configuration

Memory Management:

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:

// 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:

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.

// 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.