npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@noego/ioc

v0.2.3

Published

A self contained IoC container for Node.js

Downloads

512

Readme

@noego/ioc

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

Features

  • 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
  • Container Self-Injection: Use the provider pattern with SCOPED_CONTAINER for dynamic service creation and factories
  • Decorator Support: Use @Component, @Provider, and @Inject decorators for clean, declarative DI
  • TypeScript Support: Built with full TypeScript support for type safety
  • Sync-First Resolution: Synchronous resolution when all dependencies are sync, with async fallback when needed
  • Type-Safe Sync Mode: Generic Sync parameter on get and instance for compile-time type narrowing
  • 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

Installation

npm install @noego/ioc
# or
yarn add @noego/ioc

If you want to use decorators, also install reflect-metadata:

npm install reflect-metadata
# or
yarn add reflect-metadata

And configure TypeScript for decorator support in tsconfig.json:

{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "module": "ESNext",
    "moduleResolution": "node",
    "target": "ESNext"
  }
}

Quick Start

Follow these minimal steps to get @noego/ioc working in a TypeScript project:

  1. Install packages:

    npm install @noego/ioc reflect-metadata
  2. Configure tsconfig.json:

    {
      "compilerOptions": {
        "experimentalDecorators": true,
        "emitDecoratorMetadata": true,
        "module": "ESNext",
        "moduleResolution": "node",
        "target": "ESNext"
      },
      "include": ["src"]
    }
  3. Create an entry file (index.ts):

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

ESM vs CJS imports

  • 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'
  • If you see “does not provide an export named 'createContainer'”, your toolchain likely resolved the CommonJS build. Use this interop-safe pattern:
    • import pkg from '@noego/ioc'; const { createContainer } = pkg;
    • Or upgrade Node to a version that supports conditional exports.
  1. Run the entry file:
    npx ts-node index.ts

Getting Started

Using manual registration:

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

Using decorators:

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

Usage

Basic Usage

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

Lifetime Scopes

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

Parameter Injection

You can inject parameter values at resolution time:

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

Function Registration

You can also register functions as dependencies:

function createLogger(prefix: string) {
  return {
    log: (message: string) => console.log(`${prefix}: ${message}`)
  };
}

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

Using Decorators

The container supports decorator-based dependency injection using @Component and @Inject.

Setup

First, ensure TypeScript is configured to support decorators:

// tsconfig.json
{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    // other options...
  }
}

Also, import reflect-metadata once at your application's entry point:

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

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:

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.
  2. Manually defined scope in registerClass({ loadAs: ... }) overrides @Component({ scope: ... }).

This allows you to change behavior at registration time without modifying the decorated class.

Sync Resolution

By default, get and instance return Promise<T> | T. When your entire dependency graph is synchronous (no async factory functions), the container resolves synchronously. If you know at the call site that resolution will be sync, pass true as the second generic parameter to get a narrowed T return type:

// Default — returns Promise<T> | T
const service = container.instance(UserService);

// When you know the dependency graph is sync — returns T
const service = container.instance<UserService, true>(UserService);
const logger = container.get<Logger, true>(Logger);

This is purely a compile-time hint — no runtime behavior changes. If a dependency turns out to be async at runtime, you'll get a Promise back regardless of the type annotation.

Extending Containers

You can create a child container that inherits all the registrations from the parent but allows overriding:

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

Provider Pattern with SCOPED_CONTAINER

The @Provider decorator creates factory classes that can dynamically resolve dependencies at runtime. Use @Inject(SCOPED_CONTAINER) to inject the container itself, enabling polymorphic service creation based on runtime conditions.

import createContainer, { Provider, Inject, SCOPED_CONTAINER, IContainer, Component } from "@noego/ioc";

// Define payment processor implementations
interface PaymentProcessor {
  process(amount: number): Promise<string>;
}

@Component()
class StripeProcessor implements PaymentProcessor {
  async process(amount: number) {
    return `Processed $${amount} via Stripe`;
  }
}

@Component()
class PayPalProcessor implements PaymentProcessor {
  async process(amount: number) {
    return `Processed $${amount} via PayPal`;
  }
}

// Provider that creates the right processor at runtime
@Provider()
class PaymentProcessorFactory {
  constructor(
    @Inject(SCOPED_CONTAINER) private container: IContainer
  ) {}

  async getProcessor(type: 'stripe' | 'paypal'): Promise<PaymentProcessor> {
    switch (type) {
      case 'stripe':
        return this.container.instance(StripeProcessor);
      case 'paypal':
        return this.container.instance(PayPalProcessor);
      default:
        throw new Error(`Unknown payment processor: ${type}`);
    }
  }
}

// Service that injects the factory and changes behavior at runtime
@Component()
class OrderService {
  constructor(private paymentFactory: PaymentProcessorFactory) {}

  async processOrder(amount: number, paymentMethod: 'stripe' | 'paypal') {
    // Get the right processor based on runtime parameter
    const processor = await this.paymentFactory.getProcessor(paymentMethod);

    // Behavior changes based on what the user selected
    const result = await processor.process(amount);
    return { success: true, message: result };
  }
}

// Usage - behavior determined at runtime
const container = createContainer();
const orderService = await container.instance(OrderService);

await orderService.processOrder(100, 'stripe');  // Uses Stripe
await orderService.processOrder(200, 'paypal');  // Uses PayPal

This pattern is ideal for plugin systems, multi-tenancy, polymorphic services, and any scenario where you need to select implementations based on runtime data. The @Provider decorator defaults to Scoped lifetime, making instances scoped per container context.

Method Call Tracing and Monitoring

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

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

// 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}`);
console.log(`Total proxies created: ${stats.totalProxies}`);

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:

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

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

@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()

API Reference

Container

  • createContainer(): Creates a new IoC container
  • registerClass<T>(classDefinition, options?): Register a class
  • registerFunction(label, function, options?): Register a function
  • instance<T, Sync>(classDefinition, params?): Resolve a class instance. Pass Sync = true for sync type narrowing
  • get<T, Sync>(label, params?): Resolve a dependency by key. Pass Sync = true for sync type narrowing
  • extend(): Create a child container
  • setTracingEnabled(enabled: boolean): Enable/disable method call tracing
  • isTracingEnabled(): boolean: Check if tracing is enabled
  • setTraceRetentionMinutes(minutes: number): Set trace retention window
  • getTraces(retentionMinutes?: number): Promise<TraceRecord[]>: Get recent traces
  • getAllTraces(): Promise<TraceRecord[]>: Get all recorded traces
  • clearTraces(): Promise<void>: Clear all traces
  • exportTraces(filepath: string): Promise<void>: Export traces to JSON file
  • getTraceStatistics(): Promise<TraceStatistics>: Get trace statistics

Decorators

  • @Component(options?): Mark a class as container-managed (defaults to Transient scope)
  • @Provider(options?): Mark a class as a provider (defaults to Scoped scope, ideal for factories)
  • @Inject(token): Specify a token for a constructor parameter

Options

interface ContainerOptions {
  param?: any[];      // Dependencies or parameters
  loadAs?: LoadAs;    // Lifetime scope
}

LoadAs Enum

enum LoadAs {
  Singleton,  // Single instance throughout application
  Scoped,     // Single instance per container scope
  Transient   // New instance each time
}

Parameter

  • Parameter.create(): Create a new parameter
  • parameter.value(value): Create a parameter value

Injectable Tokens

  • SCOPED_CONTAINER: A special injection token that resolves to the current container instance. Use this in parameter arrays or with @Inject to enable the provider pattern and dynamic dependency resolution.

Component Options

interface ComponentOptions {
  scope?: LoadAs;  // Lifetime scope
}

Real-World Use Cases

Express Application

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

Running Tests

The project uses Jest for testing. To run tests:

npm test

License

ISC

Contributing

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)
  3. Install dependencies (npm install)
  4. Make your changes
  5. Run tests to ensure everything works (npm test)
  6. Commit your changes (git commit -m 'Add some amazing feature')
  7. Push to the branch (git push origin feature/amazing-feature)
  8. Open a Pull Request

Development Setup

  1. Clone the repository:

    git clone <repository-url>
    cd ioc
  2. Install dependencies:

    npm install
  3. Run tests:

    npm test

Please make sure to update tests as appropriate and follow the existing code style.