Examples
This section contains practical examples of using @nodelibraries/ioc. All examples are available in the examples/ directory and can be run directly.
Running Examples
First, make sure you have installed dependencies:
npm install
npm run buildNote: The Express examples (16-express.ts and 17-express-advanced.ts) require additional dependencies:
npm install --save-dev express @types/express ts-nodeThen run examples using ts-node:
npx ts-node examples/1-basic.tsOr compile and run:
npm run build
node dist/examples/1-basic.jsExamples Overview
Examples are organized from basic to advanced. Click on any example to see the full code and output.
Basic Examples (1-3)
1. Basic Example
Simplest usage of the IoC Container - class registration, service resolution, dependency injection, and creating scopes.
Code Snippet:
import { ServiceCollection, ServiceProvider } from '@nodelibraries/ioc';
class Logger {
log(message: string) {
console.log(`[LOG] ${message}`);
}
}
class UserService {
constructor(private logger: Logger) {
this.logger.log('UserService initialized');
}
getUsers(): string[] {
return ['Alice', 'Bob', 'Charlie'];
}
}
const services = new ServiceCollection();
services.addSingleton(Logger);
services.addScoped(UserService, [Logger]);
const provider = services.buildServiceProvider();
const userService = await provider.getRequiredService(UserService);
const users = userService.getUsers();Output:
=== Basic Example ===
[LOG] UserService initialized
[LOG] Fetching all users
Users: [ 'Alice', 'Bob', 'Charlie' ]Full Example: basic.md | Source Code
2. Interface Registration
Interface-based registration using Symbol tokens - the recommended approach for loose coupling and testability.
Code Snippet:
interface ILogger {
log(message: string): void;
}
interface IUserService {
getUsers(): string[];
}
class Logger implements ILogger {
log(message: string) {
console.log(`[LOG] ${message}`);
}
}
const ILoggerToken = Symbol('ILogger');
const IUserServiceToken = Symbol('IUserService');
services.addSingleton<ILogger>(ILoggerToken, Logger);
services.addScoped<IUserService>(IUserServiceToken, UserService, [ILoggerToken]);
const userService = await provider.getRequiredService<IUserService>(IUserServiceToken);Output:
=== Interface Registration Example ===
[LOG] <timestamp> - UserService initialized
Users: [ 'Alice', 'Bob', 'Charlie' ]Full Example: interface-registration.md | Source Code
3. String Token Registration
Using string tokens instead of Symbol tokens - simpler but less type-safe.
Code Snippet:
services.addSingleton<ILogger>('ILogger', Logger);
services.addScoped<IUserService>('IUserService', UserService, ['ILogger']);
const logger = await provider.getRequiredService<ILogger>('ILogger');Output:
=== String Token Registration Example ===
[LOG] Logger from string token
[LOG] Fetching users...
Users: [ 'Alice', 'Bob' ]Full Example: string-token.md | Source Code
Core Concepts (4-6)
4. Service Lifetimes
Demonstrates the differences between Singleton, Scoped, and Transient service lifetimes.
Code Snippet:
services.addSingleton<Counter>(CounterToken, Counter);
services.addScoped<ScopedService>(ScopedToken, ScopedService);
services.addTransient<TransientService>(TransientToken, TransientService);
// Singleton - same instance
const singleton1 = await provider.getRequiredService<SingletonService>(SingletonToken);
const singleton2 = await provider.getRequiredService<SingletonService>(SingletonToken);
console.log('Same instance?', singleton1 === singleton2); // true
// Scoped - same within scope, different across scopes
const scope1 = provider.createScope();
const scoped1 = await scope1.getRequiredService<ScopedService>(ScopedToken);
const scoped2 = await scope1.getRequiredService<ScopedService>(ScopedToken);
console.log('Same in scope?', scoped1 === scoped2); // true
// Transient - new instance every time
const transient1 = await provider.getRequiredService<TransientService>(TransientToken);
const transient2 = await provider.getRequiredService<TransientService>(TransientToken);
console.log('Same instance?', transient1 === transient2); // falseOutput:
=== Service Lifetimes Example ===
--- Singleton ---
Same instance? true
--- Scoped ---
Same instance in scope? true
Different from scope 1? true
--- Transient ---
Same instance? falseFull Example: lifetimes.md | Source Code
5. Lifecycle Hooks
Demonstrates lifecycle hooks - onInit() and onDestroy() methods for service initialization and cleanup.
Code Snippet:
class DatabaseConnection {
async onInit() {
console.log(' → DatabaseConnection.onInit() called');
await new Promise((resolve) => setTimeout(resolve, 100));
this.connected = true;
console.log(' → Database connected');
}
async onDestroy() {
console.log(' → DatabaseConnection.onDestroy() called');
this.connection = null;
console.log(' → Database connection closed');
}
}
services.addScoped<DatabaseConnection>(DatabaseToken, DatabaseConnection);
const scope = provider.createScope();
const db = await scope.getRequiredService<DatabaseConnection>(DatabaseToken);
await scope.dispose(); // onDestroy() is called automaticallyOutput:
=== Lifecycle Hooks Example ===
Getting DatabaseConnection...
→ DatabaseConnection.onInit() called
→ Database connected
Database connected? true
--- Disposing scope 1 ---
→ DatabaseConnection.onDestroy() called
→ Database connection closedFull Example: lifecycle.md | Source Code
6. Value Registration
Demonstrates registering pre-created values (JSON objects, primitives, arrays, instances, environment variables).
Code Snippet:
// JSON object
const configValue: IConfig = {
apiUrl: 'https://api.example.com',
timeout: 5000,
features: { enableCache: true, enableLogging: true },
};
services.addValue<IConfig>(configToken, configValue);
// Primitives
services.addValue<string>(apiKeyToken, 'secret-api-key-12345');
services.addValue<number>(maxRetriesToken, 3);
services.addValue<boolean>(isProductionToken, false);
// Arrays
services.addValue<string[]>(allowedOriginsToken, ['http://localhost:3000', 'https://example.com']);
// Pre-created instance
const loggerInstance = new Logger();
services.addValue<Logger>(loggerToken, loggerInstance);
const config = await provider.getRequiredService<IConfig>(configToken);Output:
=== Value Registration Examples ===
Config: {
"apiUrl": "https://api.example.com",
"timeout": 5000,
"features": {
"enableCache": true,
"enableLogging": true
}
}
API URL: https://api.example.com
Max Retries: 3
Is Production: falseFull Example: value-registration.md | Source Code
Advanced Features (7-13)
7. Generic Type Parameters
Explains the purpose of generic type parameters (<T>) - type safety, IntelliSense, and compile-time checking.
Code Snippet:
// ✅ Type-safe registration
services.addSingleton<ILogger>(ILoggerToken, Logger);
// ✅ Type inference works
const logger = await provider.getRequiredService<ILogger>(ILoggerToken);
logger.log('Hello'); // TypeScript knows this method exists
// ❌ Without generic type - no type safety
const logger2 = await provider.getRequiredService(ILoggerToken);
// logger2 type: any - no autocomplete, no type checkingOutput:
=== Generic Type Parameter Usage ===
--- 2. Return Type Inference ---
[LOG] Hello from logger
Sending email to user@example.com: Hello
✅ Generic type parameter usage:
- Provides type safety
- IDE autocomplete works
- Compile-time error checkingFull Example: generic-types.md | Source Code
8. Factory Pattern
Demonstrates factory pattern for service creation - factory functions, async initialization, conditional logic.
Code Snippet:
// Simple factory
const httpClientFactory: ServiceFactory<IHttpClient> = async (provider) => {
const config = await provider.getRequiredService<IConfig>(IConfigToken);
return new HttpClient(config);
};
services.addSingleton<IHttpClient>(IHttpClientToken, httpClientFactory);
// Async factory with initialization
const databaseFactory: ServiceFactory<IDatabase> = async (provider) => {
const connectionString = await provider.getRequiredService<string>(ConnectionStringToken);
const db = new Database(connectionString);
await db.connect(); // Async initialization
return db;
};
// Conditional factory
const loggerFactory: ServiceFactory<ILogger> = async (provider) => {
const logType = await provider.getRequiredService<string>(LogTypeToken);
return logType === 'file' ? new FileLogger() : new ConsoleLogger();
};Output:
=== Factory Pattern Examples ===
--- 1. Simple Factory Pattern ---
GET https://api.example.com/users (timeout: 5000ms)
--- 2. Factory with Async Initialization ---
Connecting to database: postgresql://localhost:5432/mydb
Database connected!
Executing query: SELECT * FROM usersFull Example: factory-pattern.md | Source Code
9. Multiple Implementations
Shows how to register and retrieve multiple implementations of the same interface using getServices().
Code Snippet:
// Register multiple implementations with same token
services.addSingleton<IMessageWriter>(IMessageWriterToken, ConsoleWriter);
services.addSingleton<IMessageWriter>(IMessageWriterToken, FileWriter);
services.addSingleton<IMessageWriter>(IMessageWriterToken, EmailWriter);
// Get all implementations
const writers = await provider.getServices<IMessageWriter>(IMessageWriterToken);
console.log(`Total writers: ${writers.length}`); // 3
writers.forEach((writer) => {
writer.write('Hello from multiple implementations!');
});
// Get last registered (default)
const lastWriter = await provider.getService<IMessageWriter>(IMessageWriterToken);Output:
=== Multiple Implementations Examples ===
Total writers: 3
Writer 1:
[CONSOLE] Hello from multiple implementations!
Writer 2:
[FILE] Hello from multiple implementations!
Writer 3:
[EMAIL] Hello from multiple implementations!Full Example: multiple-implementations.md | Source Code
10. TryAdd Pattern
Demonstrates safe registration without overriding - tryAddSingleton, tryAddScoped, tryAddTransient methods.
Code Snippet:
// TryAdd - only register if not exists
services.tryAddSingleton<ILogger>(ILoggerToken, DefaultLogger);
services.tryAddSingleton<ILogger>(ILoggerToken, CustomLogger); // Ignored - already exists
// Normal Add - overrides
services.addSingleton<ILogger>(ILoggerToken2, DefaultLogger);
services.addSingleton<ILogger>(ILoggerToken2, CustomLogger); // Overrides!
// Library pattern - preserve user overrides
function registerDefaultServices(services: ServiceCollection) {
services.tryAddSingleton<ILogger>(ILoggerToken, DefaultLogger);
}
function registerUserServices(services: ServiceCollection) {
services.addSingleton<ILogger>(ILoggerToken, CustomLogger);
}
registerUserServices(services);
registerDefaultServices(services); // User's logger is preservedOutput:
=== TryAdd Pattern Examples ===
✅ DefaultLogger registered with tryAddSingleton
⚠️ CustomLogger tryAddSingleton - ignored (already exists)
[DEFAULT] Test message
--- 3. Library/Module Registration Pattern ---
User: Registered custom logger
Library: Registered default logger
[CUSTOM] Test messageFull Example: tryadd-pattern.md | Source Code
11. Keyed Services
Shows key-based service lookup - register services with string or symbol keys and retrieve them using getKeyedService().
Code Snippet:
// Register with keys
services.addKeyedSingleton<ICache>(ICacheToken, BigCache, 'big');
services.addKeyedSingleton<ICache>(ICacheToken, SmallCache, 'small');
// Retrieve by key
const bigCache = await provider.getKeyedService<ICache>(ICacheToken, 'big');
const smallCache = await provider.getKeyedService<ICache>(ICacheToken, 'small');
// Required version with error handling
const cache = await provider.getRequiredKeyedService<ICache>(ICacheToken, 'big');Output:
=== Keyed Services Examples ===
Using keyed services:
[BIG CACHE] Setting: user:1
[BIG CACHE] Getting: user:1
[SMALL CACHE] Setting: session:abc
[SMALL CACHE] Getting: session:abcFull Example: keyed-services.md | Source Code
12. Duplicate Registration
Explains what happens when registering the same interface multiple times - last registration wins, using different tokens, and the Replace() method.
Code Snippet:
// Same token - last registration wins
services.addSingleton<ILogger>(ILoggerToken, ConsoleLogger);
services.addSingleton<ILogger>(ILoggerToken, FileLogger); // Overrides ConsoleLogger
// Different tokens - both coexist
const ConsoleLoggerToken = Symbol('ConsoleLogger');
const FileLoggerToken = Symbol('FileLogger');
services.addSingleton<ILogger>(ConsoleLoggerToken, ConsoleLogger);
services.addSingleton<ILogger>(FileLoggerToken, FileLogger);
// Replace() - explicit replacement
services.replace<ILogger>(ILoggerToken, FileLogger);Output:
=== Registering 2 Concrete Classes with Same Interface ===
✅ ConsoleLogger registered
⚠️ FileLogger registered (same token)
[FILE] Test message
📌 Result: Last registration overrides previous!Full Example: duplicate-registration.md | Source Code
13. Scope Validation
Demonstrates scope validation feature - validateScopes and validateOnBuild options to catch lifetime mismatches.
Code Snippet:
// Enable validation
const provider = services.buildServiceProvider({
validateScopes: true,
validateOnBuild: true,
});
// This will error: scoped service injected into singleton
services.addScoped<ILogger>(ILoggerToken, Logger);
services.addSingleton<IUserService>(IUserServiceToken, UserService, [ILoggerToken]);
// Error: Cannot inject scoped service into singleton
// Validate dependencies at build time
services.addSingleton<IUserService>(IUserServiceToken, UserService, [MissingToken]);
// Error: Missing dependency: Symbol(Missing) required by Symbol(IUserService)Output:
=== Scope Validation Examples ===
✅ Validation worked!
Error: Cannot inject scoped service 'Symbol(ILogger)' into singleton service
--- 5. ValidateOnBuild (Dependency Validation) ---
✅ Build-time validation works!
Error: Validation failed on build:
Missing dependency: Symbol(Missing) required by Symbol(IUserService)Full Example: scope-validation.md | Source Code
Complex Scenarios (14-15)
14. Circular Dependency Resolution
Demonstrates circular dependency resolution - singleton, scoped, and transient circular dependencies are all supported.
Code Snippet:
const ServiceAToken = Symbol('ServiceA');
const ServiceBToken = Symbol('ServiceB');
class ServiceA {
constructor(private serviceB: ServiceB) {}
getName(): string {
return 'ServiceA';
}
getServiceBName(): string {
return this.serviceB.getName();
}
getServiceB(): ServiceB {
return this.serviceB;
}
}
class ServiceB {
constructor(private serviceA: ServiceA) {}
getName(): string {
return 'ServiceB';
}
getServiceAName(): string {
return this.serviceA.getName();
}
getServiceA(): ServiceA {
return this.serviceA;
}
}
// Singleton circular dependency
services.addSingleton(ServiceAToken, ServiceA, [ServiceBToken]);
services.addSingleton(ServiceBToken, ServiceB, [ServiceAToken]);
// Scoped circular dependency
services.addScoped(ServiceAToken, ServiceA, [ServiceBToken]);
services.addScoped(ServiceBToken, ServiceB, [ServiceAToken]);
// Transient circular dependency
services.addTransient(ServiceAToken, ServiceA, [ServiceBToken]);
services.addTransient(ServiceBToken, ServiceB, [ServiceAToken]);
const serviceA = await provider.getRequiredService<ServiceA>(ServiceAToken);
console.log(serviceA.getServiceBName()); // ServiceB
console.log(serviceA.getServiceB().getServiceAName()); // ServiceAOutput:
=== Testing Singleton Circular Dependency ===
✅ Circular dependency resolved successfully!
ServiceA name: ServiceA
ServiceB name: ServiceB
ServiceA -> ServiceB name: ServiceB
ServiceB -> ServiceA name: ServiceA
Same ServiceA instance? true
Same ServiceB instance? true
=== Testing Scoped Circular Dependency ===
✅ Circular dependency resolved successfully in scope!
ServiceA name: ServiceA
ServiceB name: ServiceB
ServiceA -> ServiceB name: ServiceB
ServiceB -> ServiceA name: ServiceA
Same ServiceA instance in scope? true
Same ServiceB instance in scope? true
=== Testing Transient Circular Dependency ===
✅ Circular dependency resolved successfully for transient services!
ServiceA1 name: ServiceA
ServiceB1 name: ServiceB
ServiceA1 -> ServiceB1 name: ServiceB
ServiceB1 -> ServiceA1 name: ServiceA
Transient behavior check:
serviceA1 === serviceA2? false
serviceB1 === serviceB2? falseFull Example: circular-dependency.md | Source Code
15. Complex Dependency Chain
Demonstrates a complex dependency chain: ServiceA -> ServiceB -> ServiceC -> ServiceD, ServiceE with different lifetimes.
Code Snippet:
// Deep dependency chain: ServiceA -> ServiceB -> ServiceC -> ServiceD, ServiceE
interface IServiceD {
getName(): string;
}
interface IServiceE {
getName(): string;
}
interface IServiceC {
getName(): string;
getDependency(): IServiceD;
getOtherDependency(): IServiceE;
}
interface IServiceB {
getName(): string;
getDependency(): IServiceC;
}
interface IServiceA {
getName(): string;
getDependency(): IServiceB;
}
class ServiceD implements IServiceD {
getName(): string {
return 'ServiceD';
}
}
class ServiceE implements IServiceE {
getName(): string {
return 'ServiceE';
}
}
class ServiceC implements IServiceC {
constructor(private serviceD: IServiceD, private serviceE: IServiceE) {
console.log(`[ServiceC] Initialized with ${serviceD.getName()} and ${serviceE.getName()}`);
}
getName(): string {
return 'ServiceC';
}
getDependency(): IServiceD {
return this.serviceD;
}
getOtherDependency(): IServiceE {
return this.serviceE;
}
}
class ServiceB implements IServiceB {
constructor(private serviceC: IServiceC) {
console.log(`[ServiceB] Initialized with ${serviceC.getName()}`);
}
getName(): string {
return 'ServiceB';
}
getDependency(): IServiceC {
return this.serviceC;
}
}
class ServiceA implements IServiceA {
constructor(private serviceB: IServiceB) {
console.log(`[ServiceA] Initialized with ${serviceB.getName()}`);
}
getName(): string {
return 'ServiceA';
}
getDependency(): IServiceB {
return this.serviceB;
}
}
// Register services in dependency order (bottom-up)
services.addSingleton<IServiceD>(ServiceDToken, ServiceD);
services.addSingleton<IServiceE>(ServiceEToken, ServiceE);
services.addSingleton<IServiceC>(ServiceCToken, ServiceC, [ServiceDToken, ServiceEToken]);
services.addSingleton<IServiceB>(ServiceBToken, ServiceB, [ServiceCToken]);
services.addSingleton<IServiceA>(ServiceAToken, ServiceA, [ServiceBToken]);
// Resolve top of chain - automatically resolves entire chain
const serviceA = await provider.getRequiredService<IServiceA>(ServiceAToken);
const serviceD = serviceA.getDependency().getDependency().getDependency();
console.log(serviceD.getName()); // ServiceDOutput:
=== Complex Dependency Chain Example ===
--- Resolving ServiceA (top of chain) ---
[ServiceC] Initialized with ServiceD and ServiceE
[ServiceB] Initialized with ServiceC
[ServiceA] Initialized with ServiceB
--- Verifying Dependency Chain ---
ServiceA name: ServiceA
ServiceB name: ServiceB
ServiceC name: ServiceC
ServiceD name: ServiceD
ServiceE name: ServiceE
--- Testing Singleton Behavior ---
Same ServiceA instance? true
Same ServiceD instance? true
Same ServiceE instance? true
--- Testing Scoped Services ---
Different scopes, different ServiceA? true
Same scope, same ServiceB? trueFull Example: complex-dependency-chain.md | Source Code
Real-World Applications (16-18)
16. Service Management
Demonstrates service management features - Remove/RemoveAll() methods for dynamic service management.
Code Snippet:
interface ILogger {
log(message: string): void;
}
interface ICache {
get(key: string): string | undefined;
set(key: string, value: string): void;
}
interface IConfig {
get(key: string): string | undefined;
}
class Logger implements ILogger {
log(message: string) {
console.log(`[LOG] ${message}`);
}
}
class ConsoleLogger implements ILogger {
log(message: string) {
console.log(`[CONSOLE] ${message}`);
}
}
class BigCache implements ICache {
private data = new Map<string, string>();
get(key: string): string | undefined {
return this.data.get(key);
}
set(key: string, value: string): void {
this.data.set(key, value);
}
}
// 1. Remove services
services.addSingleton<ILogger>(ILoggerToken, Logger);
services.addSingleton<ICache>(ICacheToken, BigCache);
services.addSingleton<IConfig>(IConfigToken, Config);
console.log(`Logger exists? ${await provider.isService<ILogger>(ILoggerToken)}`); // true
console.log(`Cache exists? ${await provider.isService<ICache>(ICacheToken)}`); // true
services.remove<ILogger>(ILoggerToken);
services.removeAll<ICache>(ICacheToken); // Alias for remove
console.log(`Logger exists after remove? ${await provider.isService<ILogger>(ILoggerToken)}`); // false
console.log(`Cache exists after removeAll? ${await provider.isService<ICache>(ICacheToken)}`); // false
// 2. Test scenario - replace with mock
services.remove<ILogger>(ILoggerToken);
services.addSingleton<ILogger>(ILoggerToken, ConsoleLogger); // Mock logger
// 3. Dynamic service management
const useFileLogger = process.env.USE_FILE_LOGGER === 'true';
if (useFileLogger) {
services.remove<ILogger>(ILoggerToken);
services.addSingleton<ILogger>(ILoggerToken, FileLogger);
}Output:
=== Service Management Example ===
--- 1. Remove/RemoveAll() - Remove Services ---
Logger exists? true
Cache exists? true
Config exists? true
Logger exists after remove? false
Cache exists after remove? true
Config exists after remove? true
Cache exists after removeAll? false
Config exists after removeAll? true
--- 2. Test Scenario - Mock Replacement ---
[CONSOLE] This is from mock logger
--- 3. Dynamic Service Management ---
[LOG] Dynamic logger
--- 4. Service Cleanup Pattern ---
Before cleanup:
Logger: true
Cache: true
Config: true
After cleanup:
Logger: false
Cache: false
Config: falseFull Example: service-management.md | Source Code
17. Express Integration
Complete example of using the IoC container with Express.js - request-scoped services, middleware, and RESTful API.
Code Snippet:
import express, { Request, Response } from 'express';
import { ServiceCollection, ServiceProvider } from '@nodelibraries/ioc';
interface ILogger {
log(message: string): void;
}
interface IUserRepository {
findAll(): User[];
findById(id: number): User | null;
create(user: { name: string; email: string }): User;
}
interface IUserService {
getUsers(): User[];
getUserById(id: number): User | null;
createUser(name: string, email: string): User;
}
class Logger implements ILogger {
log(message: string) {
console.log(`[${new Date().toISOString()}] ${message}`);
}
}
class UserRepository implements IUserRepository {
constructor(private logger: ILogger) {
this.logger.log('UserRepository initialized');
}
findAll() {
this.logger.log('Repository: Finding all users');
return this.users;
}
// ... other methods
}
class UserService implements IUserService {
constructor(private repository: IUserRepository, private logger: ILogger) {
this.logger.log('UserService initialized');
}
getUsers() {
this.logger.log('Service: Getting all users');
return this.repository.findAll();
}
// ... other methods
}
// Setup DI Container
const services = new ServiceCollection();
const ILoggerToken = Symbol('ILogger');
const IUserRepositoryToken = Symbol('IUserRepository');
const IUserServiceToken = Symbol('IUserService');
services.addSingleton<ILogger>(ILoggerToken, Logger);
services.addScoped<IUserRepository>(IUserRepositoryToken, UserRepository, [ILoggerToken]);
services.addScoped<IUserService>(IUserServiceToken, UserService, [IUserRepositoryToken, ILoggerToken]);
const provider = services.buildServiceProvider();
// Express App
const app = express();
app.use(express.json());
// Middleware: Create scoped container per request
app.use(async (req: Request, res: Response, next) => {
const scope = provider.createScope();
(req as any).scope = scope;
res.on('finish', async () => {
await scope.dispose();
});
next();
});
// Routes
app.get('/users', async (req: Request, res: Response) => {
const scope: ServiceProvider = (req as any).scope;
const userService = await scope.getRequiredService<IUserService>(IUserServiceToken);
const users = userService.getUsers();
res.json(users);
});
app.get('/users/:id', async (req: Request, res: Response) => {
const scope: ServiceProvider = (req as any).scope;
const userService = await scope.getRequiredService<IUserService>(IUserServiceToken);
const id = parseInt(req.params.id, 10);
const user = userService.getUserById(id);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
res.json(user);
});
app.post('/users', async (req: Request, res: Response) => {
const scope: ServiceProvider = (req as any).scope;
const userService = await scope.getRequiredService<IUserService>(IUserServiceToken);
const { name, email } = req.body;
if (!name || !email) {
return res.status(400).json({ error: 'Name and email are required' });
}
const newUser = userService.createUser(name, email);
res.status(201).json(newUser);
});
app.listen(3000, () => {
console.log('🚀 Express server with IoC Container running on http://localhost:3000');
});Output:
🚀 Express server with IoC Container running on http://localhost:3000
Available endpoints:
GET http://localhost:3000/users
GET http://localhost:3000/users/:id
POST http://localhost:3000/users
[<timestamp>] UserRepository initialized
[<timestamp>] UserService initialized
[<timestamp>] Repository: Finding all users
[<timestamp>] Service: Getting all usersFull Example: express.md | Source Code
17. Advanced Express Integration
More complex Express.js application with multiple services, middleware, authentication, error handling, and request context.
Code Snippet:
import express, { Request, Response, NextFunction } from 'express';
import { ServiceCollection, ServiceProvider } from '@nodelibraries/ioc';
interface ILogger {
log(message: string, level?: string): void;
error(message: string, error?: Error): void;
}
interface IConfig {
get(key: string): string | undefined;
}
interface IUserRepository {
findById(id: number): Promise<User | null>;
findAll(): Promise<User[]>;
create(user: Partial<User>): Promise<User>;
}
interface IUserService {
getUserById(id: number): Promise<User | null>;
getAllUsers(): Promise<User[]>;
createUser(data: CreateUserDto): Promise<User>;
}
interface IAuthService {
validateToken(token: string): Promise<boolean>;
getUserIdFromToken(token: string): Promise<number | null>;
}
interface IRequestContext {
userId?: number;
requestId: string;
startTime: number;
}
// Setup services
const services = new ServiceCollection();
services.addSingleton<IConfig>(IConfigToken, Config);
services.addSingleton<ILogger>(ILoggerToken, Logger, [IConfigToken]);
services.addScoped<IUserRepository>(IUserRepositoryToken, UserRepository, [ILoggerToken]);
services.addScoped<IUserService>(IUserServiceToken, UserService, [IUserRepositoryToken, ILoggerToken]);
services.addScoped<IAuthService>(IAuthServiceToken, AuthService, [IConfigToken, ILoggerToken]);
const provider = services.buildServiceProvider();
// Request context middleware
function createRequestScopeMiddleware(provider: ServiceProvider) {
return async (req: Request, res: Response, next: NextFunction) => {
const requestId = `req-${Date.now()}`;
const scope = provider.createScope();
(req as any).scope = scope;
(req as any).requestContext = { requestId, startTime: Date.now() };
const logger = await scope.getRequiredService<ILogger>(ILoggerToken);
logger.log(`Request started: ${req.method} ${req.path} [${requestId}]`);
res.on('finish', async () => {
const duration = Date.now() - (req as any).requestContext.startTime;
logger.log(`Request finished: ${req.method} ${req.path} [${requestId}] - ${duration}ms`);
await scope.dispose();
});
next();
};
}
// Authentication middleware
function createAuthMiddleware() {
return async (req: Request, res: Response, next: NextFunction) => {
const scope: ServiceProvider = (req as any).scope;
const authService = await scope.getRequiredService<IAuthService>(IAuthServiceToken);
const token = req.headers.authorization;
if (!(await authService.validateToken(token))) {
return res.status(401).json({ error: 'Unauthorized' });
}
const userId = await authService.getUserIdFromToken(token);
(req as any).requestContext.userId = userId;
next();
};
}
// Error handling middleware
function createErrorHandler() {
return async (err: Error, req: Request, res: Response, next: NextFunction) => {
const scope: ServiceProvider = (req as any).scope;
const logger = await scope.getRequiredService<ILogger>(ILoggerToken);
logger.error('Request error', err);
res.status(500).json({ error: 'Internal server error' });
};
}
const app = express();
app.use(express.json());
app.use(createRequestScopeMiddleware(provider));
// Protected routes
app.get('/users', createAuthMiddleware(), async (req: Request, res: Response) => {
const scope: ServiceProvider = (req as any).scope;
const userService = await scope.getRequiredService<IUserService>(IUserServiceToken);
const users = await userService.getAllUsers();
res.json(users);
});
app.use(createErrorHandler());
app.listen(3000, () => {
console.log('🚀 Server running on http://localhost:3000');
});Output:
🚀 Server running on http://localhost:3000
[INFO] <timestamp> - Request started: GET /users [req-...]
[INFO] <timestamp> - UserRepository initialized
[INFO] <timestamp> - UserService initialized
[INFO] <timestamp> - Fetching all users [req-...]
[INFO] <timestamp> - Request finished: GET /users [req-...] - <duration>msFull Example: express-advanced.md | Source Code
18. Dependency Tree Visualization
Visualize and analyze service dependency trees. See the complete dependency hierarchy, detect circular dependencies, and get structured tree data.
Code Snippet:
import { ServiceCollection } from '@nodelibraries/ioc';
const services = new ServiceCollection();
// ... register services ...
// Visualize dependency tree
console.log(services.visualizeDependencyTree(IUserServiceToken));
// Get tree as object
const tree = services.getDependencyTree(IUserServiceToken);Output:
└── Symbol(IUserService) [SINGLETON]
├── Symbol(IUserRepository) [SINGLETON]
│ ├── Symbol(IDatabase) [SINGLETON]
│ └── Symbol(ILogger) [SINGLETON]
└── Symbol(ILogger) [SINGLETON]Full Example: dependency-tree.md | Source Code
19. Circular Dependency Detection
Detect and visualize all circular dependencies in your service collection. Works with simple and complex circular dependencies, and multiple independent cycles.
Code Snippet:
import { ServiceCollection } from '@nodelibraries/ioc';
const services = new ServiceCollection();
// ... register services with circular dependencies ...
// Detect circular dependencies
const circularDeps = services.getCircularDependencies();
console.log(services.visualizeCircularDependencies());Output:
Found 1 circular dependency/ies:
Circular Dependency 1:
Symbol(ServiceA) → Symbol(ServiceB) → Symbol(ServiceC) → Symbol(ServiceA)Full Example: circular-dependency-detection.md | Source Code
JavaScript Examples
The library works with both TypeScript and JavaScript. All features are available in JavaScript, but without compile-time type safety.
JS Basic Example
File: js/js-basic.js | Documentation
Basic JavaScript usage - service registration, resolution, dependency injection, and scopes.
Run:
node examples/js/js-basic.jsJS Advanced Example
File: js/js-advanced.js | Documentation
Advanced JavaScript features - factory pattern, keyed services, multiple implementations, value registration, service checking, and TryAdd pattern.
Run:
node examples/js/js-advanced.jsKey Points:
- All features work in JavaScript (same API as TypeScript)
- No compile-time type safety
- Runtime validation recommended in constructors
- CommonJS
require()syntax (ES Modules also supported)
Learning Path
- Start with basics: Examples 1-3 introduce the fundamental concepts
- Understand lifetimes: Example 4 is crucial for understanding how services are managed
- Learn lifecycle: Example 5 shows how to handle initialization and cleanup
- Explore advanced features: Examples 7-13 cover more sophisticated patterns
- Handle edge cases: Examples 14-15 demonstrate complex scenarios
- Build real applications: Examples 16-18 show practical usage
- JavaScript users: Check out
js/js-basic.jsandjs/js-advanced.jsfor JavaScript examples
Notes
- All examples include expected console output at the end of each file
- Examples are self-contained and can be run independently
- Each example focuses on specific features to avoid confusion
- Examples progress from simple to complex, building on previous concepts