Skip to content

nodelibraries/ioc

Repository files navigation

@nodelibraries/ioc

Type-Safe IoC Container for Node.js, TypeScript and JavaScript

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

npm version npm downloads License Node.js Version TypeScript


✨ Features

Core Features

  • 🎯 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

Advanced Features

  • 🏭 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() and onDestroy() 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

JavaScript Support

  • πŸ“œ Full JavaScript Support - All features work in JavaScript (CommonJS and ES Modules)
  • πŸ”§ JSDoc Support - Comprehensive JSDoc comments for IntelliSense and autocomplete
  • ⚠️ Runtime Validation Recommended - Use validateOnBuild and validateScopes for better error detection

πŸš€ Quick Start

Installation

npm install @nodelibraries/ioc

TypeScript Example

import { 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();

JavaScript Example (CommonJS)

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();
})();

πŸ“š Documentation


🎯 Why @nodelibraries/ioc?

✨ Clean & Simple

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]);

πŸ”’ Type-Safe by Design

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 exists

πŸš€ Production Ready

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

πŸ”„ Enhanced Circular Dependency Support

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

πŸ“– Key Concepts

Service Lifetimes

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

Registration Methods

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

Service Resolution

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

πŸ” Advanced Features

Dependency Tree Visualization

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 nodes

Circular Dependency Detection

Detect 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)
}

Lifecycle Hooks

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 called

πŸ“¦ Examples

We 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.ts

See examples/README.md for detailed descriptions and running instructions.


πŸ”„ Comparison with .NET Core DI

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 ⚠️ May fail βœ… Works for all lifetimes
Dependency Tree Visualization ❌ βœ…
Circular Dependency Detection ❌ βœ…

πŸ› οΈ Requirements

  • Node.js >= 18.0.0 (LTS recommended)
  • TypeScript >= 5.0.0 (optional, but recommended)

πŸ“ License

ISC License - see LICENSE file for details.


πŸ‘€ Author

ylcnfrht


πŸ“ž Support


🀝 Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add some amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

Made with ❀️ for the TypeScript/Node.js community

⭐ Star us on GitHub β€’ πŸ“¦ npm β€’ πŸ“š Documentation

About

Type-Safe IoC Container for Node.js, TypeScript and JavaScript

Topics

Resources

License

Contributing

Stars

Watchers

Forks

Releases

No releases published

Sponsor this project

Packages

No packages published

Contributors 2

  •  
  •