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.
1. Automatic Cycle Resolution (Recommended)
You can configuremini-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.
- Local isolation: Overrides and bindings defined on a fork never pollute the parent.
- Shared Singletons: Resolving a parent-bound singleton from a fork returns the same instance the parent holds.
- Disposal: Calling
fork.clear()disposes only the fork's local singletons, leaving the parent container untouched.
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
- Introduced a global DI container accessible via
DIstatic proxy methods (DI.bind,DI.get,DI.has, etc.), eliminating the need to explicitly instantiatenew DI()for most use cases. - Added
DI.runInContext(callback)to execute a function within a scoped, isolated DI context while preserving the static global API shape, perfect for testing isolation or contextual flows. - Improved
removeSubModulebehavior and added new comprehensive tests for the sub-module lifecycle. - Fixed formatting and dependency tracking bugs for
Containerinstances containingDILiteralandDIFactorydependencies. - Ensured graceful fallback and cleanup of cyclic Proxies when errors occur during automatic circular dependency resolution.
- Greatly simplified
README.mdand moved extensive reference documentation and guides to the official GitHub Pages site. - Implemented extensive test coverage improvements across edge-cases.
1.13.5 and 1.13.6
- Published the documentation online
1.13.4
- Added SECURITY.md
- Included github actions
- Executed
npm auditand addressed all vulnerabilities
1.13.3
- Small typescript interface correction
1.13.2
- Fixed a critical bug in
di.clear()where uninitialized Proxy instances (fromlateResolve: true) were being eagerly instantiated just to check for adisposemethod. Now, uninitialized proxies are correctly bypassed during clear.
1.13.1
- Fixed
di.has()anddi.getBinding()not working for Container bindings. - Fixed TypeScript overload resolution for
di.get()when passing a Container. It now correctly infersT[]return type. - Fixed the
get()fallback behavior for Containers; it now correctly returns an empty array[]whenfallbackToEmptyListis truthy and the container has no bindings, instead of returningtrue.
1.13.0
- Added
Containerclass — allows binding multiple injectables, factories, or tokens to a single container reference. When resolved, the container returns an array containing all resolved items. - Individual container items retain their own configuration (e.g.,
isSingleton,lateResolve,eager). - Improved
getAllTypeScript typings using conditional resolution, accurately returningT[]when resolving aContainer<T>while maintaining inference and backwards compatibility for existing tuple bindings. - Officially documented the
eager: booleanflag in both JSDoc andREADME.md, which is intended to eventually replace manuallateResolveflags. - Container dependencies natively support resolving custom factory functions
(di) => Tfor inline configurations. - Updated
DependencyGraphmodule to identify and reportContainercontents and their corresponding downstream dependencies.
1.12.0
- Added
di.fork()— creates a childDIinstance that delegates unresolved keys to its parent; parent singletons are shared, fork-local bindings stay isolated, andclear()on the fork never touches the parent. Supports arbitrary fork depth - Added
di.unbind(injectable)— removes a single binding and its cached singleton instance; callsdispose()on the instance if the method exists, then removes both the binding and the cached value - Added
{ eager: true }option tobind()— when set on a singleton binding, the instance is created immediately at bind time instead of lazily on firstget(); silently ignored for transient bindings clear()now callsdispose()on every cached singleton instance (and on sub-module instances recursively) before wiping the container, giving services a chance to release resources; errors thrown bydispose()are silently ignored- Added
di.getDependencyGraph()/DI.getDependencyGraph(di)— returns a serializable graph object ({ nodes, edges, cycles }) describing all registered bindings, their dependency descriptors, directed edges, and any detected circular-dependency cycles - Added
di.formatDependencyGraph(opts?)/DI.formatDependencyGraph(graph, opts?)— renders a dependency graph as a human-readable text report; pass{ header: false }to suppress the title and cycles-summary section - Added
bin/analyze.mjsCLI — runnpx mini-inject analyze <file>to print a dependency report for any module that exports aDIinstance; supports--format=text|json,--export=<name>, and--no-header - Dep descriptors distinguish between
injectablekeys,Literal<value>,Factory<name>, andnull(custom factory function — deps cannot be statically determined) - Token keys are displayed as
Token<description>in all outputs - Bindings from attached sub-modules are included in the graph and marked with
isSubModule: true - New TypeScript types:
DepDescriptor,GraphNode,GraphEdge,DependencyGraph,FormatGraphOptions
1.11.0
- Added
DI.autoResolveCircularDependencies(true/false)— global static flag that automatically detects and resolves circular dependencies at runtime for all instances, without needing anylateResolveflags on bindings - Added
instance.autoResolveCircularDependencies(true/false)— per-instance flag with the same behavior, only affecting that specificDIinstance. The global flag takes precedence - When neither auto mode nor
lateResolveis used and a cycle is encountered,get()now throws a descriptive error listing the full dependency chain (e.g."Circular dependency detected: A → B → A") with instructions on how to fix it - Updated TypeScript declarations and JSDoc for all affected methods
1.10.1 and 1.10.2
- Improved
getAllmethod signatures to use named parameters instead of tuple types - Fixed TypeScript compatibility with the latest TypeScript versions
- Better type inference for
getAllcalls with 15+ parameters using spread syntax
1.10
- Added the
clear()method to reset DI containers, bindings, and sub-modules - The
clear()method recursively clears all sub-modules to ensure complete cleanup - Useful for testing scenarios and reconfiguring the entire dependency injection container
1.9
- Added support for Tokens through
di.tokenmethod
1.8
- Added factory for dependencies
1.7
- Added sub-modules through the method
subModule - Added the method
hasto test if there is a binding for an injectable - Binding now works without any parameters for constructable classes. Calling just
di.bind(A)now works as if it weredi.bind(A, [])
1.6
- Added literals for dependencies
1.5
- Binding with an empty dependency array now automatically set lateResolve flag to
false - Added the method
getBindingfor accessing the inner works of the library