Modern Dependency Injection for Node.js - A powerful, enterprise-ready DI container with fluent API while maintaining the elegant {a,b,c}
destructuring syntax.
β
Fluent/Chainable API - Modern, readable service registration
β
Destructuring Support - Keep your elegant {service, config}
syntax
β
π·οΈ Tag-based Discovery - Find services by tags with AND/OR logic
β
Scoped Dependencies - Request/session scoped services
β
Factory Functions - With full dependency injection
β
Circular Dependency Detection - Automatic detection and helpful errors
β
Lifecycle Hooks - beforeCreate, afterCreate, etc.
β
TypeScript Ready - Full type definitions included
β
Zero Dependencies - Lightweight and secure
β
Enterprise Ready - Plugin systems, layered architecture, strategy patterns
β
Security Hardened - Prototype pollution protection & memory limits
The Dependency Injection pattern separates object instantiation from business logic, providing:
- π Explicit dependencies - Clear understanding of service relationships
- β»οΈ Code reuse - Services decoupled from specific implementations
- π§ͺ Easy testing - Mock dependencies effortlessly
- ποΈ Better architecture - Clean, maintainable code structure
# Install from npm (recommended)
npm install sdijs
# Or clone the repository for development
git clone https://github.com/mau-io/sdijs.git
cd sdijs
# Install dependencies
npm install
# Run tests
npm test
# Run example
node example.js
Requirements: Node.js 16+ (ES Modules support)
import SDI, { createContainer } from 'sdijs';
// Create container with modern API - RECOMMENDED
const container = createContainer({
verbose: true,
autoBinding: true,
strictMode: false
});
// Sample services with {destructuring} - MAINTAINED!
class Database {
constructor({config, logger}) { // β Still works!
this.config = config;
this.logger = logger;
}
async query(sql) {
this.logger.info(`Executing: ${sql}`);
return [];
}
}
class UserService {
constructor({database, logger}) { // β Destructuring preserved!
this.database = database;
this.logger = logger;
}
async findUser(id) {
return this.database.query(`SELECT * FROM users WHERE id = ${id}`);
}
}
// β¨ NEW FLUENT API - Much more powerful!
container
.value('config', { dbUrl: 'postgresql://...' })
.factory('logger', ({config}) => ({
info: (msg) => console.log(`[INFO] ${msg}`),
error: (msg) => console.error(`[ERROR] ${msg}`)
}))
.singleton(Database)
.singleton(UserService);
// Use your services
const userService = container.resolve('userService');
const user = await userService.findUser(123);
import SDI, { createContainer } from 'sdijs';
// Method 1: Constructor
const container = new SDI({
verbose: false, // Enable/disable logging
autoBinding: true, // Auto-bind class methods
strictMode: false, // Strict registration rules
allowOverrides: false, // Allow service overrides
maxServices: 1000, // Memory limit for services
maxInstances: 5000, // Memory limit for instances
maxScopes: 100, // Memory limit for scopes
maxHooksPerEvent: 50 // Memory limit for hooks
});
// Method 2: Factory function (RECOMMENDED)
const container = createContainer({ verbose: true });
// Basic registration with lifecycle
container.singleton(UserService); // Singleton class
container.transient(NotificationService); // New instance each time
container.value('config', configObject); // Direct value
// Advanced registration with builder pattern
container
.register(AdminService)
.withTag('admin')
.withTag('security')
.when(() => process.env.NODE_ENV === 'production')
.asSingleton();
// Factory functions with DI
container.factory('emailService', ({config, logger}) => {
return {
send: async (to, subject, body) => {
logger.info(`Sending email to ${to}`);
// Implementation here
return { sent: true, to, subject };
}
};
}).asSingleton();
// Multiple service registration
container.registerAll({
config: configObject,
logger: loggerInstance,
cache: cacheService
});
Lifecycle | Description | Method |
---|---|---|
Singleton | One instance, cached globally | .singleton() |
Transient | New instance every time | .transient() |
Scoped | One instance per scope | .register().asScoped() |
Value | Direct value, no instantiation | .value() |
// Basic resolution
const userService = container.resolve('userService');
// Multiple resolution
const {database, logger, config} = container.resolveAll([
'database', 'logger', 'config'
]);
// Lazy resolution
const getUserService = container.getResolver('userService');
const service = getUserService(); // Resolved when called
// Scoped resolution (see Scopes section)
const requestService = requestScope.resolve('requestService');
Perfect for web applications with request/session-specific data:
// Create scopes
const requestScope = container.createScope('request');
const sessionScope = container.createScope('session');
// Register scoped services
class RequestContext {
constructor({}) {
this.requestId = Math.random().toString(36).substr(2, 9);
this.startTime = Date.now();
}
getElapsed() {
return Date.now() - this.startTime;
}
}
container.register(RequestContext).asScoped();
// Use in different scopes
const ctx1 = requestScope.resolve('requestContext');
const ctx2 = requestScope.resolve('requestContext');
console.log(ctx1 === ctx2); // true - same instance within scope
const ctx3 = sessionScope.resolve('requestContext');
console.log(ctx1 === ctx3); // false - different scope
// Clean up when done
requestScope.dispose();
container
.hook('beforeCreate', ({service}) => {
console.log(`Creating ${service.name}`);
})
.hook('afterCreate', ({service, instance}) => {
console.log(`Created ${service.name}`);
})
.hook('beforeResolve', ({name}) => {
console.log(`Resolving ${name}`);
})
.hook('afterResolve', ({name, result}) => {
console.log(`Resolved ${name}`);
});
// Clean up hooks when needed
container.clearHooks('beforeCreate');
π₯ NEW in v2.0: Tag-based service discovery enables powerful architectural patterns like plugin systems, environment-specific services, and layered architectures.
// Register services with multiple tags
container
.register(DatabaseRepository)
.withTag('repository')
.withTag('persistence')
.withTag('production')
.withTag('database')
.asSingleton();
container
.register(ApiRepository)
.withTag('repository')
.withTag('http')
.withTag('external')
.withTag('api')
.asSingleton();
container
.register(CacheService)
.withTag('cache')
.withTag('performance')
.withTag('memory')
.asSingleton();
// β¨ POWERFUL TAG-BASED SERVICE DISCOVERY
// 1. Find services with ALL specified tags (AND mode - default)
const prodRepositories = container.getServicesByTags(['repository', 'production']);
// Returns: [{ name: 'databaseRepository', service: {...}, tags: [...], lifecycle: 'singleton' }]
// 2. Find services with ANY specified tags (OR mode)
const dataServices = container.getServicesByTags(['repository', 'cache'], 'OR');
// Returns: All repositories AND cache services
// 3. Get just the service names (simplified)
const repoNames = container.getServiceNamesByTags(['repository']);
// Returns: ['databaseRepository', 'apiRepository']
// 4. Resolve services directly by tags
const resolvedRepos = container.resolveServicesByTags(['repository']);
// Returns: [{ name: 'databaseRepository', instance: <resolved>, tags: [...] }]
// 5. Get all available tags (sorted)
const allTags = container.getAllTags();
console.log(allTags);
// ['api', 'cache', 'database', 'external', 'http', 'memory', 'persistence', 'production', 'repository']
// 6. Get services grouped by tag
const grouped = container.getServicesByTag();
console.log(grouped.repository); // ['databaseRepository', 'apiRepository']
console.log(grouped.cache); // ['cacheService']
π Tag Discovery Methods:
Method | Purpose | Returns |
---|---|---|
getServicesByTags(tags, mode) |
Full service metadata | Array<{name, service, tags, lifecycle}> |
getServiceNamesByTags(tags, mode) |
Just service names | string[] |
resolveServicesByTags(tags, mode, scope) |
Resolved instances | Array<{name, instance, tags}> |
getAllTags() |
All unique tags | string[] (sorted) |
getServicesByTag() |
Services grouped by tag | Record<string, string[]> |
Tags enable enterprise-grade architectural patterns. Here are 5 powerful use cases:
// π 1. ENVIRONMENT-SPECIFIC SERVICES
// Register different implementations for different environments
container.register(MockPaymentService).withTags('payment', 'development').asSingleton();
container.register(StripePaymentService).withTags('payment', 'production').asSingleton();
container.register(TestEmailService).withTags('email', 'test').asSingleton();
container.register(SendGridEmailService).withTags('email', 'production').asSingleton();
// Load environment-specific services
const env = process.env.NODE_ENV || 'development';
const paymentService = container.getServicesByTags(['payment', env])[0];
const emailServices = container.resolveServicesByTags(['email', env]);
// ποΈ 2. LAYERED ARCHITECTURE
// Organize services by architectural layers
container.register(UserRepository).withTags('repository', 'data-layer').asSingleton();
container.register(ProductRepository).withTags('repository', 'data-layer').asSingleton();
container.register(UserService).withTags('service', 'business-layer').asSingleton();
container.register(OrderService).withTags('service', 'business-layer').asSingleton();
container.register(UserController).withTags('controller', 'presentation-layer').asSingleton();
// Load entire layers
const dataLayer = container.resolveServicesByTags(['data-layer']);
const businessLayer = container.resolveServicesByTags(['business-layer']);
// π 3. PLUGIN SYSTEM
// Dynamic plugin discovery and initialization
container.register(AuthPlugin).withTags('plugin', 'security').asSingleton();
container.register(LoggingPlugin).withTags('plugin', 'monitoring').asSingleton();
container.register(CachePlugin).withTags('plugin', 'performance').asSingleton();
// Initialize all plugins
const plugins = container.resolveServicesByTags(['plugin']);
plugins.forEach(plugin => {
console.log(`Initializing plugin: ${plugin.name}`);
plugin.instance.initialize();
});
// Load specific plugin categories
const securityPlugins = container.resolveServicesByTags(['plugin', 'security']);
// β‘ 4. STRATEGY PATTERN IMPLEMENTATION
// Multiple payment strategies
container.register(CreditCardPayment).withTags('payment', 'strategy', 'card').asSingleton();
container.register(PayPalPayment).withTags('payment', 'strategy', 'paypal').asSingleton();
container.register(CryptoPayment).withTags('payment', 'strategy', 'crypto').asSingleton();
// Get all payment strategies
const allPaymentStrategies = container.getServicesByTags(['payment', 'strategy']);
console.log(`Available payment methods: ${allPaymentStrategies.map(s => s.name).join(', ')}`);
// Get specific payment type
const cardPayments = container.resolveServicesByTags(['payment', 'card']);
// π© 5. FEATURE FLAGS AND CAPABILITIES
// Conditional feature loading
container.register(BasicAnalytics).withTags('analytics', 'basic').asSingleton();
container.register(AdvancedAnalytics).withTags('analytics', 'premium').asSingleton();
container.register(MonitoringService).withTags('monitoring', 'optional').asSingleton();
container.register(A11yService).withTags('accessibility', 'optional').asSingleton();
// Load features based on subscription level
const userTier = 'premium'; // from user session
const analytics = container.getServicesByTags(['analytics', userTier]);
// Load optional features if enabled
const optionalFeatures = container.resolveServicesByTags(['optional']);
optionalFeatures.forEach(feature => {
if (isFeatureEnabled(feature.name)) {
feature.instance.enable();
}
});
π‘ Pro Tips for Tag Usage:
- Use hierarchical tags:
['service', 'user-service']
for better organization - Combine environment + feature:
['cache', 'redis', 'production']
- Use descriptive names:
['repository', 'read-only']
vs['repo', 'ro']
- Group by capability:
['serializer', 'json']
,['serializer', 'xml']
- Version your APIs:
['api', 'v1']
,['api', 'v2']
container
.register(MockEmailService)
.when(() => process.env.NODE_ENV === 'test')
.asSingleton();
container
.register(SendGridEmailService)
.when(() => process.env.NODE_ENV === 'production')
.asSingleton();
// Strict mode prevents accidental overrides
const strictContainer = createContainer({ strictMode: true });
strictContainer.singleton(Service, 'myService');
strictContainer.singleton(NewService, 'myService'); // β Throws error
// Explicit override
strictContainer
.register(NewService, 'myService')
.override()
.asSingleton(); // β
Works
SDI v2.0 includes enterprise-grade security features:
// Prototype pollution protection
class VulnerableService {
constructor(deps) {
deps.__proto__; // β Throws: Dangerous property access blocked
deps.constructor; // β Throws: Dangerous property access blocked
}
}
// Memory limits prevent DoS attacks
const limitedContainer = createContainer({
maxServices: 100, // Max registered services
maxInstances: 500, // Max cached instances
maxScopes: 10, // Max concurrent scopes
maxHooksPerEvent: 20 // Max hooks per event
});
// Input validation on all methods
container.value('', 'test'); // β Throws: Service name must be a non-empty string
container.factory('test', 'not-a-function'); // β Throws: Factory must be a function
The key feature that makes SDI special - your destructuring syntax remains unchanged:
// β
All of these still work exactly the same!
class UserService {
constructor({database, logger, config, emailService}) {
this.database = database;
this.logger = logger;
this.config = config;
this.emailService = emailService;
}
}
class PaymentService {
constructor({userService, database, logger}) {
// Your existing code doesn't change!
}
}
// Factory functions too
const utils = ({config, logger}) => {
return {
formatUser: (user) => `${user.name} <${user.email}>`,
logAction: (action) => logger.info(action)
};
};
// Service management
container.has('serviceName'); // Check if registered
container.unregister('serviceName'); // Remove service
container.clear(); // Clear all services
container.getServiceNames(); // List all service names
// Scope management
const scope = container.createScope('myScope');
scope.dispose(); // Clean up scope
scope.getInstances(); // Get all instances in scope
// Hook management
container.clearHooks('beforeCreate'); // Remove all hooks for event
SDI v2.0 provides helpful error messages:
// β Service not found
container.resolve('nonExistent');
// Error: Service 'nonExistent' not found. Did you forget to register it?
// β Circular dependency
container.singleton(ServiceA).singleton(ServiceB);
// Error: Circular dependency detected: serviceA β serviceB β serviceA
// β Read-only dependencies
class Service {
constructor(deps) {
deps.someValue = 'test'; // Error: Dependencies are read-only
}
}
// β Memory limits exceeded
container.value('service1000', {}); // Error: Memory limit exceeded for services. Max: 1000
// β Security violations
class BadService {
constructor(deps) {
deps.__proto__; // Error: Dangerous property access blocked: '__proto__'
}
}
Old API (v1.x) | New API (v2.0) | Notes |
---|---|---|
new sdijs() |
createContainer() |
Recommended factory |
$Inject.addSingleton() |
container.singleton() |
Fluent API |
$Inject.addTransient() |
container.transient() |
Fluent API |
$Inject.addValue() |
container.value() |
Fluent API |
require('sdijs') |
import SDI from 'sdijs' |
ES Modules |
Good news: Your service classes with {destructuring}
need zero changes! π
- ES Modules only - No more CommonJS support
- Node.js 16+ - Requires modern Node.js
- Import syntax - Must use ES6 imports
- New API - Old
$Inject
global removed
Check out example.js
for a comprehensive demonstration of all features:
# If you cloned the repository
node example.js
# If you installed via npm, create your own example file with:
# import { createContainer } from 'sdijs';
# (See Quick Start section for complete example)
Explore the examples/
directory with 9 specialized examples:
Note: Examples use relative imports (
from '../../index.js'
) since they run from the repository. In your projects, usefrom 'sdijs'
instead.
# If you cloned the repository
cd examples
# Run individual examples
npm run example:basic # Basic DI concepts
npm run example:factory # Factory functions
npm run example:scopes # Scoped dependencies & hooks
npm run example:strategy # Strategy pattern with tags
npm run example:decorator # Decorator pattern
npm run example:microservices # Enterprise microservices
npm run example:express # Express.js integration
npm run example:testing # Testing with mocks (8/8 tests pass)
npm run example:tags # Advanced tag-based discovery
# Run all examples at once
npm run example:all
The examples include:
- Basic service registration and resolution
- Scoped dependencies and lifecycle hooks
- Factory functions with full DI
- Design patterns (Strategy, Decorator)
- Enterprise patterns (Microservices communication)
- Web application integration (Express.js)
- Comprehensive testing with mocks
- π·οΈ Advanced tag-based service discovery (NEW!)
- Security features demonstration
Full TypeScript support included with index.d.ts
:
import SDI, { SDIOptions, ServiceBuilder, LIFECYCLE } from 'sdijs';
interface IUserService {
findUser(id: number): Promise<User>;
}
interface User {
id: number;
name: string;
email: string;
}
const container = new SDI({ verbose: true });
const userService = container.resolve<IUserService>('userService');
SDI v2.0 is designed for performance:
- Lazy resolution - Services created only when needed
- Efficient caching - Singleton instances cached with WeakMap
- Memory limits - Configurable limits prevent memory leaks
- Auto-binding - Optional method binding with caching
- Minimal overhead - Zero dependencies, pure JavaScript
- π·οΈ Fast tag discovery - Optimized Set operations for tag matching
- Scalable architecture - Performance tested with 55 services and 100+ tags
- Quick operations - Tag discovery <100ms, registration <1000ms
SDI v2.0 - Modern DI that grows with your application while keeping your code clean, secure, and testable. π