A self contained IoC container for Node.js
npm install @noego/iocA lightweight, flexible Inversion of Control (IoC) container for Node.js and TypeScript applications, providing support for multiple dependency lifetime scopes, parameter injection, and both class and function registration with full type safety.
- Dual Module Support: Compatible with CommonJS and ES Modules
- TypeScript & Typings: Built in TypeScript with bundled declaration files
- Multiple Lifetime Scopes: Support for Singleton, Transient, and Scoped dependencies
- Class & Function Registration: Register both classes and functions as dependencies
- Parameter Injection: Inject parameter values at resolution time
- Container Extension: Create child containers that inherit parent registrations
- Decorator Support: Use @Component and @Inject decorators for clean, declarative DI
- TypeScript Support: Built with full TypeScript support for type safety
- Async Resolution: Support for asynchronous dependency resolution
- Method Call Tracing: Automatic tracing of method calls with performance metrics and dependency hierarchies
- Trace Analytics: Export and analyze traces, track statistics, and monitor dependency interactions
- Lightweight: Small footprint with minimal external dependencies
``bash`
npm install @noego/iocor
yarn add @noego/ioc
If you want to use decorators, also install reflect-metadata:
`bash`
npm install reflect-metadataor
yarn add reflect-metadata
And configure TypeScript for decorator support in tsconfig.json:
`json`
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"module": "ESNext",
"moduleResolution": "node",
"target": "ESNext"
}
}
1. Install packages:
`bash`
npm install @noego/ioc reflect-metadata
2. Configure tsconfig.json:`
json`
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"module": "ESNext",
"moduleResolution": "node",
"target": "ESNext"
},
"include": ["src"]
}
3. Create an entry file (index.ts):`
typescript
import 'reflect-metadata';
import createContainer, { Component } from '@noego/ioc';
@Component()
class ExampleService {}
async function bootstrap() {
const container = createContainer();
container.registerClass(ExampleService);
const svc = await container.instance(ExampleService);
console.log('Service instance:', svc);
}
bootstrap();
`
- Modern Node (>=14.13, >=16 recommended) and bundlers that honor package.exports can use either:import { createContainer } from '@noego/ioc'
- import createContainer from '@noego/ioc'
- import pkg from '@noego/ioc'; const { createContainer } = pkg;
- If you see “does not provide an export named 'createContainer'”, your toolchain likely resolved the CommonJS build. Use this interop-safe pattern:
-
- Or upgrade Node to a version that supports conditional exports.
4. Run the entry file:
`bash`
npx ts-node index.ts
`typescript
import createContainer from "@noego/ioc";
// Create a container
const container = createContainer();
// Register your dependencies
container.registerClass(Database);
container.registerClass(UserRepository, { param: [Database] });
// Resolve and use
async function main() {
const repo = await container.instance(UserRepository);
const users = repo.getUsers();
}
main();
`
`typescript
import createContainer, { Component, Inject } from "@noego/ioc";
import 'reflect-metadata'; // Required when using decorators
@Component({ scope: LoadAs.Singleton })
class Database {
connect() {
return "Connected to DB";
}
}
@Component()
class UserRepository {
constructor(private db: Database) {}
getUsers() {
this.db.connect();
return ["User1", "User2"];
}
}
// Create container and register classes
const container = createContainer();
container.registerClass(Database);
container.registerClass(UserRepository);
// Resolve and use
async function main() {
const repo = await container.instance(UserRepository);
const users = repo.getUsers();
}
main();
`
`typescript
import createContainer from "@noego/ioc";
// Create a container
const container = createContainer();
// Define classes
class Database {
connect() {
return "Connected to DB";
}
}
class UserRepository {
constructor(private db: Database) {}
getUsers() {
this.db.connect();
return ["User1", "User2"];
}
}
class UserService {
constructor(private repo: UserRepository) {}
getAllUsers() {
return this.repo.getUsers();
}
}
// Register dependencies
container.registerClass(Database);
container.registerClass(UserRepository, { param: [Database] });
container.registerClass(UserService, { param: [UserRepository] });
// Resolve dependencies
async function run() {
const userService = await container.instance(UserService);
const users = userService.getAllUsers();
console.log(users); // ["User1", "User2"]
}
run();
`
The container supports three different lifetime scopes:
1. Transient (default): A new instance is created every time the dependency is resolved
2. Singleton: Only one instance is created and reused throughout the application
3. Scoped: A single instance is created per container scope
`typescript
import { LoadAs } from "@noego/ioc";
// Register a singleton
container.registerClass(Database, { loadAs: LoadAs.Singleton });
// Register a scoped dependency
container.registerClass(UserRepository, {
param: [Database],
loadAs: LoadAs.Scoped
});
`
You can inject parameter values at resolution time:
`typescript
import { Parameter } from "@noego/ioc";
class User {
constructor(public id: number, public name: string) {}
}
// Create parameters
const USER_ID = Parameter.create();
const USER_NAME = Parameter.create();
// Register with parameters
container.registerClass(User, { param: [USER_ID, USER_NAME] });
// Resolve with parameter values
async function createUser() {
const user = await container.instance(User, [
USER_ID.value(1),
USER_NAME.value("John")
]);
console.log(user.id, user.name); // 1, "John"
}
`
You can also register functions as dependencies:
`typescript${prefix}: ${message}
function createLogger(prefix: string) {
return {
log: (message: string) => console.log()
};
}
const PREFIX = Parameter.create();
// Register function
container.registerFunction("logger", createLogger, {
param: [PREFIX]
});
// Resolve function
async function useLogger() {
const logger = await container.get("logger", [PREFIX.value("APP")]);
logger.log("Application started"); // "APP: Application started"
}
`
The container supports decorator-based dependency injection using @Component and @Inject.
#### Setup
First, ensure TypeScript is configured to support decorators:
`json`
// tsconfig.json
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
// other options...
}
}
Also, import reflect-metadata once at your application's entry point:
`typescript`
// index.ts or main.ts
import 'reflect-metadata';
// ... rest of your code
#### Component Decorator
Use @Component to mark a class as a component with an optional scope:
`typescript
import { Component, LoadAs } from '@noego/ioc';
@Component() // Default is Transient
class UserService {
// ...
}
@Component({ scope: LoadAs.Singleton })
class DatabaseService {
// ...
}
@Component({ scope: LoadAs.Scoped })
class RequestContext {
// ...
}
`
#### Inject Decorator
Use @Inject to specify tokens for interface dependencies or to override constructor parameter types:
`typescript
import { Component, Inject } from '@noego/ioc';
// Define an interface
interface ILogger {
log(message: string): void;
}
// Create a token for the interface
const LoggerToken = Symbol('ILogger');
// Implement the interface
@Component({ scope: LoadAs.Singleton })
class ConsoleLogger implements ILogger {
log(message: string) {
console.log(message);
}
}
// Use @Inject to specify which implementation to use
@Component()
class UserService {
constructor(
// Use @Inject with a token
@Inject(LoggerToken) private logger: ILogger,
// Regular parameter - resolved by type
private database: DatabaseService
) {}
createUser() {
this.logger.log('Creating user...');
// ...
}
}
// Register
const container = createContainer();
container.registerClass(DatabaseService);
container.registerClass(ConsoleLogger);
container.registerFunction(LoggerToken, () => container.instance(ConsoleLogger));
container.registerClass(UserService);
// Resolve
const service = await container.instance(UserService);
`
#### Override Priority
Manual registration options take precedence over decorators:
1. Manually defined parameters in registerClass({ param: [...] }) override constructor parameter types and @Inject annotations.registerClass({ loadAs: ... })
2. Manually defined scope in overrides @Component({ scope: ... }).
This allows you to change behavior at registration time without modifying the decorated class.
You can create a child container that inherits all the registrations from the parent but allows overriding:
`typescript
// Create parent container
const parentContainer = createContainer();
parentContainer.registerClass(Database);
// Create child container
const childContainer = parentContainer.extend();
// Override in child container
childContainer.registerClass(Database, { / different configuration / });
// Parent container still uses the original registration
// Child container uses the new registration
`
The container supports automatic tracing of method calls on resolved instances. This is useful for debugging, monitoring, and understanding dependency interactions in your application.
#### Enabling Tracing
`typescript
const container = createContainer();
// Enable tracing
container.setTracingEnabled(true);
// Optional: Set trace retention (default is 5 minutes)
container.setTraceRetentionMinutes(10);
// Register your classes
container.registerClass(DatabaseService);
container.registerClass(UserService);
// When instances are resolved, method calls are automatically traced
const service = await container.get(UserService);
service.getUsers(); // This call will be traced
`
#### Retrieving Traces
`typescript
// Get recent traces within retention window
const traces = await container.getTraces();
console.log(traces);
// Get all traces ever recorded
const allTraces = await container.getAllTraces();
// Get trace statistics
const stats = await container.getTraceStatistics();
console.log(Total method calls traced: ${stats.totalTraces});Total proxies created: ${stats.totalProxies}
console.log();`
#### How Tracing Works
When tracing is enabled:
1. Automatic Wrapping: Each resolved instance is wrapped in a JavaScript Proxy that intercepts method calls
2. Call Recording: Every method call is recorded with:
- Method name and parameters
- Return value or error (if thrown)
- Execution duration in milliseconds
- Parent-child dependency relationships
3. Zero Overhead When Disabled: When tracing is disabled, instances are not wrapped and there's no performance impact
4. Database Storage: Traces are stored in-memory using sql.js (pure JavaScript SQLite)
5. Automatic Cleanup: Old traces are automatically cleaned up based on retention settings
#### Trace Statistics
The trace statistics provide insights into your application's dependency interactions:
`typescript
const stats = await container.getTraceStatistics();
// Example output:
// {
// totalTraces: 42, // Total method calls recorded
// totalProxies: 5, // Total unique instances traced
// proxiesByClass: {
// UserService: 1,
// DatabaseService: 1,
// UserRepository: 1
// },
// methodCallsByProxy: {
// 1: 12, // Proxy 1 had 12 method calls
// 2: 8, // Proxy 2 had 8 method calls
// // ...
// }
// }
`
#### Exporting and Analyzing Traces
`typescript
// Export traces to JSON for analysis
const exported = await TraceLoggerModule.exportTracesToJson();
// or use container method
await container.exportTraces('./traces.json');
// Clear traces
await container.clearTraces();
`
#### Tracing with Dependency Hierarchies
When an instance depends on other instances, the tracing system records the parent-child relationships:
`typescript
@Component()
class Database {
query() { return 'data'; }
}
@Component()
class UserService {
constructor(private db: Database) {}
getUsers() { return this.db.query(); }
}
const container = createContainer();
container.setTracingEnabled(true);
container.registerClass(Database);
container.registerClass(UserService);
const service = await container.get(UserService);
await service.getUsers();
// Traces will show the call hierarchy:
// UserService.getUsers() -> Database.query()
`
- createContainer(): Creates a new IoC containerregisterClass
- : Register a classregisterFunction(label, function, options?)
- : Register a functioninstance
- : Resolve a class instanceget
- : Resolve a dependency by keyextend()
- : Create a child containersetTracingEnabled(enabled: boolean)
- : Enable/disable method call tracingisTracingEnabled(): boolean
- : Check if tracing is enabledsetTraceRetentionMinutes(minutes: number)
- : Set trace retention windowgetTraces(retentionMinutes?: number): Promise
- : Get recent tracesgetAllTraces(): Promise
- : Get all recorded tracesclearTraces(): Promise
- : Clear all tracesexportTraces(filepath: string): Promise
- : Export traces to JSON filegetTraceStatistics(): Promise
- : Get trace statistics
- @Component(options?): Mark a class as container-managed@Inject(token)
- : Specify a token for a constructor parameter
`typescript`
interface ContainerOptions {
param?: any[]; // Dependencies or parameters
loadAs?: LoadAs; // Lifetime scope
}
`typescript`
enum LoadAs {
Singleton, // Single instance throughout application
Scoped, // Single instance per container scope
Transient // New instance each time
}
- Parameter.create(): Create a new parameterparameter.value(value)
- : Create a parameter value
`typescript`
interface ComponentOptions {
scope?: LoadAs; // Lifetime scope
}
`typescript
import express from 'express';
import createContainer, { LoadAs } from '@noego/ioc';
// Create services
class ConfigService {
getConfig() {
return { port: 3000 };
}
}
class DatabaseService {
constructor(private config: ConfigService) {}
connect() {
console.log('Connected to database');
return {};
}
}
class UserRepository {
constructor(private db: DatabaseService) {}
findAll() {
return [{ id: 1, name: 'User 1' }];
}
}
class UserController {
constructor(private repo: UserRepository) {}
getUsers(req, res) {
const users = this.repo.findAll();
res.json(users);
}
}
// Setup container
const container = createContainer();
container.registerClass(ConfigService, { loadAs: LoadAs.Singleton });
container.registerClass(DatabaseService, { param: [ConfigService], loadAs: LoadAs.Singleton });
container.registerClass(UserRepository, { param: [DatabaseService] });
container.registerClass(UserController, { param: [UserRepository] });
// Create express app
const app = express();
// Setup routes using the container
app.get('/users', async (req, res) => {
const controller = await container.instance(UserController);
controller.getUsers(req, res);
});
// Start server
async function bootstrap() {
const config = await container.instance(ConfigService);
app.listen(config.getConfig().port, () => {
console.log(Server running on port ${config.getConfig().port});
});
}
bootstrap();
`
The project uses Jest for testing. To run tests:
`bash`
npm test
ISC
Contributions are welcome! Here's how you can contribute to this project:
1. Fork the repository
2. Create your feature branch (git checkout -b feature/amazing-feature)npm install
3. Install dependencies ()npm test
4. Make your changes
5. Run tests to ensure everything works ()git commit -m 'Add some amazing feature'
6. Commit your changes ()git push origin feature/amazing-feature
7. Push to the branch ()
8. Open a Pull Request
1. Clone the repository:
`bash`
git clone
cd ioc
2. Install dependencies:
`bash`
npm install
3. Run tests:
`bash``
npm test
Please make sure to update tests as appropriate and follow the existing code style.