Introduction

Minimalistic dependency injection implementation without decorators (about 200kb after installing).

mini-inject is offered as both CommonJS and ESModule files and there are no dependencies except for testing.

The goal is to offer dependency injection as complete as possible with the most simple code that anyone can read.

Everything runs synchronously and there is no need to add a bunch of decorators everywhere. It works as intended and there is no black-box or magic.

Installation

mini-inject is available as the package mini-inject on npm:

npm i mini-inject

The package provides both CJS (.cjs) and ESM (.mjs) files along with type definitions.

There is no need to install any type definition package; we provide all type declarations on .d.ts files, and all public methods are documented with examples.

Support

We are actively working on this project. Use the GitHub page for opening issues or discussions.

Getting Started

This page covers basic usage, bindings, tokens, container configuration, and container resetting.

Basic Usage

Here is a quick example of defining dependencies and resolving them:

const {DI} = require('mini-inject');
// or using ESM
import {DI} from 'mini-inject';

class A {
    constructor(value) {
        this.value = value;
    }
}

class B {
    value = 'B';
}

class C {
    constructor (a, b) {
        this.a = a;
        this.b = b;
    }
}

const di = new DI();

// Bind classes A, B and C by assigning a function for instantiation
di
    .bind(A, (di) => new A(0))                       // A is a singleton dependency
    .bind(B, (di) => new B(), {isSingleton: false}) // B is transient (not a singleton)
    .bind(C, (di) => new C(di.get(A), di.get(B)));  // C is a singleton dependency

// Alternatively, let mini-inject auto-resolve from dependency arrays
di
    .bind(A, [di.literal(0)])          // A singleton with a literal value (0)
    .bind(B, [], {isSingleton: false}) // B transient
    .bind(C, [A, B]);                  // C resolving A and B automatically

const a = di.get(A);
console.log(a.value); // 0
a.value = 10;

const c = di.get(C);
console.log(c.a.value); // 10
console.log(c.b.value); // B

// Fetch multiple dependencies at once
const [a2, b2, c2] = di.getAll(A, B, C);

Global DI and Contextual Isolation

mini-inject offers a global DI container accessible directly via the DI class static methods. You don't need to instantiate new DI() for simple applications.

In addition to .bind() and .get(), the global DI class exposes static equivalents for .has(), .getAll(), .getResolver(), .getBinding(), .unbind(), and .clear().

import { DI } from 'mini-inject';

// Bind and get directly from the global container
DI.bind(A, [DI.literal(0)]);
const a = DI.get(A); // 0

For tests or isolated scopes, you can temporarily isolate the global DI using DI.runInContext(callback). Any DI.bind or DI.get calls inside the callback will use a fresh, isolated DI instance that is automatically cleaned up when the callback finishes.

DI.runInContext(() => {
    // This binding only exists inside this function
    DI.bind(B, [DI.literal(1)]);
    const b = DI.get(B);
}); // Original global context is safely restored

Handling Missing Bindings

If you try to resolve a class or key that has not been bound, mini-inject will throw an error:

class D {}
try {
    const d = di.get(D);
} catch(err) {
    console.error(err); // Error: No binding for injectable "D"
}

// You can provide a fallback value to avoid exceptions:
let d = di.get(D, 1);     // Returns 1
d = di.get(D, undefined); // Returns undefined

Tokens

Tokens give you more control over binding keys, allowing you to avoid naming conflicts when different modules export classes with identical names.

// Suppose we import two classes of the same name:
import {C as C1} from './c1';
import {C as C2} from './c2';

const di = new DI();
const tokenC1 = di.token(C1, 'C1');
const tokenC2 = di.token(C2, 'C2');

di.bind(tokenC1, []);
di.bind(tokenC2, []);

const [c1, c2] = di.getAll(tokenC1, tokenC2);
console.log(c1 === c2); // false

// Attempting to retrieve them without the token will throw an error:
di.get(C1); // Throws 'No binding for injectable "C1"'

Containers

Containers let you group multiple bindings under a single key. When resolved, the container returns an array containing all its resolved bindings. Each bound element preserves its configuration.

class PluginA {}
class PluginB {}
class PluginC {}

const di = new DI();
const plugins = di.container('plugins');

di.bind(PluginA, []);
di.bind(PluginB, []);

// Bind elements to the container:
di.bind(plugins, PluginA, { isSingleton: true });
di.bind(plugins, PluginB, { isSingleton: false });
di.bind(plugins, () => new PluginC(), { isSingleton: true });

// Resolving a container returns an array of all instances:
const list = di.get(plugins);
console.log(list.length); // 3
console.log(list[0] instanceof PluginA); // true

The options parameter on bind() supports eager: true. If eager is true, mini-inject instantiates the singleton immediately instead of lazily.

Clearing DI Containers

You can reset a DI instance using clear(). This clears all bindings, container configurations, caches, and recursively clears sub-modules:

const di = new DI();
di.bind(A, []);

console.log(di.has(A)); // true

// Clear container (calls .dispose() on singletons if they implement it)
di.clear();

console.log(di.has(A)); // false

Advanced Features

This section covers automatic circular dependency resolution, sub-modules, and isolated scoped containers (forks).

Circular Dependencies

mini-inject provides multiple ways to resolve circular dependencies without throwing stack overflow errors.

You can configure mini-inject to auto-detect circular references and resolve them transparently with lazy proxies:
const {DI} = require('mini-inject');

class ServiceA {
    constructor(b) { this.b = b; }
    greet() { return `A (b is ${this.b.name})`; }
}

class ServiceB {
    constructor(a) { this.a = a; }
    get name() { return 'B'; }
    greet() { return `B (a is ${this.a.greet()})`; }
}

// Enable auto-resolution globally
DI.autoResolveCircularDependencies(true);

const di = new DI();
di.bind(ServiceA, (di) => new ServiceA(di.get(ServiceB)));
di.bind(ServiceB, (di) => new ServiceB(di.get(ServiceA)));

const a = di.get(ServiceA);
console.log(a instanceof ServiceA); // true
console.log(a.greet());             // A (b is B)

You can also enable it per-instance:

const di = new DI();
di.autoResolveCircularDependencies(true);

2. Manual Late Resolve (lateResolve: true)

If auto-resolution is off, you can instruct mini-inject to proxy specific bindings:
di.bind(A1, [A2], {lateResolve: true});
di.bind(A2, [A1]); // A2 will receive a late resolver Proxy for A1

3. Resolver Injection (getResolver)

You can inject a resolver function to retrieve dependencies lazily on-demand:
di.bind(B1, [B2]);
di.bind(B2, [di.literal(di.getResolver(B1))]);

// Or using factory functions:
di.bind(B2, [di.factory((_di) => _di.getResolver(B1))]);

Sub-Modules

Sub-modules allow you to modularize your dependency configurations. A parent container can look up bindings from attached sub-modules, but sub-modules cannot access parent container bindings.

const {DI} = require('mini-inject');
const di = new DI();
class ParentService {}
di.bind(ParentService);

const sub = new DI();
class SubService {}
sub.bind(SubService);

// Attach the sub-module
di.subModule(sub);

// Parent can resolve both:
di.get(ParentService); // Works
di.get(SubService);    // Works (searched recursively in sub)

// Sub-module cannot resolve parent bindings:
sub.get(ParentService); // Throws 'No binding for injectable "ParentService"'

Scoped Containers (Forks)

di.fork() creates a child container that inherits all parent bindings while maintaining its own isolated scope.

This pattern is ideal for request scoping in web servers or test isolation.

const appDI = new DI();
appDI.bind(DbPool, []);
appDI.bind(UserRepo, [DbPool]);

// Create request-scoped fork
const reqDI = appDI.fork();
reqDI.bind(RequestContext, () => new RequestContext(req));
reqDI.bind(OrderService, [UserRepo, RequestContext]);

// DB pool is shared with the main container
console.log(reqDI.get(DbPool) === appDI.get(DbPool)); // true

// Clear local request singletons at the end of the request
reqDI.clear();

Dependency Graph Analyzer

mini-inject includes a dependency graph analyzer to inspect registered bindings, detect circular references statically, and optimize container configurations.

Programmatic API

You can generate a serializable graph object or render a human-readable text report:

const {DI} = require('mini-inject');

class AuthService {}
class UserService {}
class OrderService {}

const di = new DI();
di.bind(AuthService, []);
di.bind(UserService, [AuthService]);
di.bind(OrderService, [UserService, AuthService]);

// 1. Get serializable graph data
const graph = di.getDependencyGraph();
// Returns: { nodes: [...], edges: [...], cycles: [] }
console.log(JSON.stringify(graph, null, 2));

// 2. Format as a clean text report
console.log(di.formatDependencyGraph());

Output text report:

mini-inject dependency graph — 3 binding(s), 0 cycle(s)
=======================================================

AuthService           [singleton]
UserService           [singleton]               AuthService
OrderService          [singleton]               UserService, AuthService

Static API Variants

You can also use static methods to generate or format graphs:

const graph = DI.getDependencyGraph(di);
const textReport = DI.formatDependencyGraph(graph, { header: false });


Command Line Interface (CLI)

You can run the analyzer CLI command directly on files that export a DI instance.

npx mini-inject analyze <path-to-file>

The targets should export the DI instance as default export (export default di / module.exports = di) or a named export.

CLI Options

Option Description
--format=<text|json> Selects report formatting. Defaults to text.
--no-header Suppresses summary titles and cycle blocks from text output.
--export=<name> Picks a specific named export from the file if multiple exist.

Example:

npx mini-inject analyze ./src/container.js --no-header

Changelog

1.14.0

1.13.5 and 1.13.6

1.13.4

1.13.3

1.13.2

1.13.1

1.13.0

1.12.0

1.11.0

1.10.1 and 1.10.2

1.10

1.9

1.8

1.7

1.6

1.5