A lightweight, production-ready Inversion of Control (IoC) container inspired by .NET Core's dependency injection system.
Built with TypeScript for full type safety β’ Zero dependencies β’ No decorators required
π Documentation β’ π‘ Examples β’ π API Reference β’ β GitHub
- π― Fully Type-Safe - Complete TypeScript support with type inference and compile-time safety
- π¦ Zero Dependencies - Lightweight, pure TypeScript implementation
- π« No Decorators - Clean, readable code without decorator pollution
- π Multiple Lifetimes - Singleton, Scoped, and Transient service lifetimes
- ποΈ Automatic DI - Seamless dependency resolution and injection
- π Factory Pattern - Support for factory functions and async initialization
- π’ Multiple Implementations - Register and retrieve multiple implementations of the same interface
- π Keyed Services - Key-based service lookup with
getRequiredKeyedService - β TryAdd Pattern - Safe registration without overriding existing services
- π‘οΈ Scope Validation - Detect lifetime mismatches at build time
- π Service Checking - Check service existence with
isService()without resolving - ποΈ Service Management - Remove, replace, and manage services dynamically
- π Lifecycle Hooks -
onInit()andonDestroy()callbacks for initialization and cleanup - π Value Registration - Register pre-created values (JSON, primitives, instances)
- π Circular Dependencies - Automatic resolution for all lifetimes (including Transient, unlike .NET Core)
- π Dependency Tree Visualization - Visualize and analyze service dependency trees
- π Circular Dependency Detection - Detect and visualize all circular dependencies
- π Full JavaScript Support - All features work in JavaScript (CommonJS and ES Modules)
- π§ JSDoc Support - Comprehensive JSDoc comments for IntelliSense and autocomplete
β οΈ Runtime Validation Recommended - UsevalidateOnBuildandvalidateScopesfor better error detection
npm install @nodelibraries/iocimport { ServiceCollection, ServiceProvider } from '@nodelibraries/ioc';
// Define interfaces
interface ILogger {
log(message: string): void;
}
interface IUserService {
getUsers(): string[];
}
// Implement services
class Logger implements ILogger {
log(message: string) {
console.log(`[LOG] ${message}`);
}
}
class UserService implements IUserService {
constructor(private logger: ILogger) {}
getUsers(): string[] {
this.logger.log('Fetching users...');
return ['Alice', 'Bob'];
}
}
// Setup container
const services = new ServiceCollection();
const ILoggerToken = Symbol('ILogger');
const IUserServiceToken = Symbol('IUserService');
// Register services
// β οΈ IMPORTANT: If a class constructor has parameters (dependencies), you MUST provide them in the dependencies array,
// and dependencies must appear in the array in the exact same order as the constructor parameters.
services.addSingleton<ILogger>(ILoggerToken, Logger); // No dependencies - constructor has no parameters
services.addScoped<IUserService>(IUserServiceToken, UserService, [ILoggerToken]); // Has dependency - MUST provide [ILoggerToken]
// Build provider
const provider = services.buildServiceProvider();
// Use services
const scope = provider.createScope();
const userService = await scope.getRequiredService<IUserService>(IUserServiceToken);
const users = userService.getUsers();
// Cleanup
await scope.dispose();const { ServiceCollection, ServiceProvider } = require('@nodelibraries/ioc');
class Logger {
log(message) {
console.log(`[LOG] ${message}`);
}
}
class UserService {
constructor(logger) {
this.logger = logger;
}
getUsers() {
this.logger.log('Fetching users...');
return ['Alice', 'Bob'];
}
}
const services = new ServiceCollection();
const ILoggerToken = Symbol('ILogger');
const IUserServiceToken = Symbol('IUserService');
// β οΈ IMPORTANT: If a class constructor has parameters (dependencies), you MUST provide them in the dependencies array,
// and dependencies must appear in the array in the exact same order as the constructor parameters.
services.addSingleton(ILoggerToken, Logger);
services.addScoped(IUserServiceToken, UserService, [ILoggerToken]);
const provider = services.buildServiceProvider();
(async () => {
const scope = provider.createScope();
const userService = await scope.getRequiredService(IUserServiceToken);
const users = userService.getUsers();
await scope.dispose();
})();- π Getting Started Guide - Learn the basics
- π API Reference - Complete API documentation
- π‘ Examples - 19+ practical examples with code
- π JavaScript Support - JavaScript-specific documentation
No decorators, no annotations, no framework lock-in. Your code remains pure and framework-agnostic.
// Clean, simple registration
services.addSingleton<ILogger>(ILoggerToken, Logger);
services.addScoped<IUserService>(IUserServiceToken, UserService, [ILoggerToken]);Built from the ground up for TypeScript. Full type inference, autocomplete, and compile-time safety.
// Full type safety with autocomplete
const logger = await provider.getRequiredService<ILogger>(ILoggerToken);
logger.log('Hello'); // β
TypeScript knows this method existsBattle-tested features including scope validation, lifecycle hooks, and comprehensive error handling. Enable validation in development to catch issues early:
// Build with validation (recommended for development)
const provider = services.buildServiceProvider({
validateScopes: true, // Catch lifetime mismatches (e.g., scoped service in singleton)
validateOnBuild: true, // Validate all dependencies exist at build time
});Note: Both options default to
false. Enable them explicitly for validation. For detailed explanations and examples, see the documentation.
Circular dependencies are automatically resolved for all service lifetimes, including Transient services (which .NET Core doesn't support).
// Circular dependencies work seamlessly for Singleton, Scoped, and Transient
class ServiceA {
constructor(private serviceB: ServiceB) {}
}
class ServiceB {
constructor(private serviceA: ServiceA) {} // β
Works for all lifetimes!
}
services.addSingleton(ServiceA, ServiceA, [ServiceB]);
services.addSingleton(ServiceB, ServiceB, [ServiceA]);
// β
No errors - circular dependencies are automatically resolved| Lifetime | Description | Use Case |
|---|---|---|
| Singleton | One instance for the entire application | Loggers, Configuration, Caches |
| Scoped | One instance per scope | Request-scoped services, Unit of Work |
| Transient | New instance every time | Validators, Calculators, Stateless services |
// 1. Class registration (no dependencies - constructor has no parameters)
services.addSingleton(Logger);
// 2. Interface registration with dependencies
// β οΈ IMPORTANT: If UserService constructor requires ILogger, you MUST provide [ILoggerToken]
services.addScoped<IUserService>(IUserServiceToken, UserService, [ILoggerToken]);
// 3. Factory pattern (supports async initialization)
services.addSingleton<IHttpClient>(IHttpClientToken, async (provider) => {
const config = await provider.getRequiredService<IConfig>(IConfigToken);
return new HttpClient(config.apiUrl);
});
// 4. Value registration (pre-created instances)
services.addValue<IConfig>(IConfigToken, { apiUrl: 'https://api.example.com' });
// 5. Keyed services (multiple implementations with keys)
services.addKeyedSingleton<ICache>(ICacheToken, BigCache, 'big');
services.addKeyedSingleton<ICache>(ICacheToken, SmallCache, 'small');
// 6. TryAdd pattern (safe registration - won't override existing)
services.tryAddSingleton<ILogger>(ILoggerToken, Logger); // Only registers if not already registered
// 7. Service management
services.remove(ILoggerToken); // Remove service
services.replace(ILoggerToken, NewLogger); // Replace with new implementation// 1. Optional resolution (returns undefined if not found)
const logger = await provider.getService<ILogger>(ILoggerToken);
if (logger) {
logger.log('Service found');
}
// 2. Required resolution (throws if not found)
const userService = await provider.getRequiredService<IUserService>(IUserServiceToken);
// 3. Get all implementations (for multiple registrations)
const writers = await provider.getServices<IMessageWriter>(IMessageWriterToken);
// Returns array of all registered implementations
// 4. Keyed service resolution
const cache = await provider.getRequiredKeyedService<ICache>(ICacheToken, 'big');
// 5. Check if service exists (without resolving)
if (await provider.isService<ILogger>(ILoggerToken)) {
// Service is registered
}Visualize and analyze your service dependency trees:
// Visualize dependency tree as formatted string
console.log(services.visualizeDependencyTree(IUserServiceToken));
// Output:
// βββ Symbol(IUserService) [SINGLETON]
// βββ Symbol(IUserRepository) [SINGLETON]
// β βββ Symbol(IDatabase) [SINGLETON]
// βββ Symbol(ILogger) [SINGLETON]
// Get tree as structured object
const tree = services.getDependencyTree(IUserServiceToken);
console.log(tree.dependencies); // Array of dependency nodesDetect and visualize all circular dependencies in your service collection:
// Detect all circular dependencies
const circularDeps = services.getCircularDependencies();
if (circularDeps.length > 0) {
console.log(services.visualizeCircularDependencies());
// Output:
// Found 1 circular dependency/ies:
// Circular Dependency 1:
// Symbol(ServiceA) β Symbol(ServiceB) β Symbol(ServiceA)
}Handle service initialization and cleanup with lifecycle hooks:
class DatabaseConnection {
async onInit() {
// Called after instance creation
await this.connect();
}
async onDestroy() {
// Called when scope/provider is disposed
await this.disconnect();
}
}
services.addScoped(DatabaseConnection);
const scope = provider.createScope();
const db = await scope.getRequiredService(DatabaseConnection);
// onInit() is automatically called
await scope.dispose();
// onDestroy() is automatically calledWe provide 19+ comprehensive examples covering all features:
| Category | Examples | Topics |
|---|---|---|
| Basic | 1-3 | Basic usage, interface registration, string tokens |
| Core Concepts | 4-6 | Service lifetimes, lifecycle hooks, value registration |
| Advanced | 7-13 | Generic types, factory pattern, multiple implementations, keyed services, scope validation |
| Complex | 14-15 | Circular dependencies, complex dependency chains |
| Real-World | 16-17 | Service management, Express.js integration |
| Analysis | 18-19 | Dependency tree visualization, circular dependency detection |
Run an example:
npx ts-node examples/1-basic.tsSee examples/README.md for detailed descriptions and running instructions.
This container is inspired by .NET Core's dependency injection system but designed for TypeScript/Node.js.
| Feature | .NET Core DI | @nodelibraries/ioc |
|---|---|---|
| Singleton Lifetime | β | β |
| Scoped Lifetime | β | β |
| Transient Lifetime | β | β |
| Factory Pattern | β | β |
| Multiple Implementations | β | β |
| Keyed Services | β | β |
| TryAdd Pattern | β | β |
| Scope Validation | β | β |
| Circular Dependencies | β Works for all lifetimes | |
| Dependency Tree Visualization | β | β |
| Circular Dependency Detection | β | β |
- Node.js >= 18.0.0 (LTS recommended)
- TypeScript >= 5.0.0 (optional, but recommended)
ISC License - see LICENSE file for details.
ylcnfrht
- GitHub: @ylcnfrht
- β Buy me a coffee - If you find this project helpful, consider supporting it!
- π Documentation
- π¬ GitHub Issues
- π§ GitHub Discussions
- β Buy me a coffee - If you find this project helpful, consider supporting it!
Contributions are welcome! Please feel free to submit a Pull Request.
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add some amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
Made with β€οΈ for the TypeScript/Node.js community