tiny-di.js
v1.0.2
Published
Lightweight dependency injection container
Downloads
501
Readme
tiny-di.js
A lightweight, type-safe Dependency Injection container for TypeScript and Node.js. Define your dependencies programmatically with full type inference — no decorators required.
Features
- 3 lifetime strategies — singleton, transient, and scoped (per-request)
- Multiple provider types — class constructor, value, factory, and self-resolution
- Automatic dependency injection — declare imports; the container resolves them
- Circular dependency detection — DFS-based cycle detection with descriptive error messages
- Scoped hierarchies — per-request scopes for Express or similar frameworks
- Type-safe — full TypeScript generics from registration to resolution
- No decorators — pure programmatic API
Installation
npm install tiny-di.js
# or
pnpm add tiny-di.js
# or
yarn add tiny-di.jsQuick Start
import { Container } from "tiny-di.js";
class Logger {
log(msg: string) {
console.log(msg);
}
}
class Database {
constructor(private logger: Logger) {}
query(sql: string) {
this.logger.log(`Executing: ${sql}`);
}
}
const container = new Container();
container.singleton(Logger).toSelf();
container.singleton(Database).import(Logger).toSelf();
const db = container.get(Database);
db.query("SELECT 1"); // "Executing: SELECT 1"API
Container
const container = new Container();| Method | Description |
|---|---|
| singleton<T>(token) | Register a singleton provider (one instance per container) |
| transient<T>(token) | Register a transient provider (new instance each time) |
| scoped<T>(token) | Register a scoped provider (one instance per Scope) |
| get<T>(token) | Resolve a dependency |
| createScope(id?) | Create a child Scope |
EntryBuilder (fluent registration)
Every registration method returns an EntryBuilder that lets you chain configuration:
| Method | Description |
|---|---|
| .import(...tokens) | Declare dependencies to inject |
| .to(class) | Map token to a class constructor |
| .to(value) | Map token to an already-created value |
| .toSelf() | Use the token itself as the constructor |
| .toFactory(fn) | Provide a factory function |
| .none() | Register with no provider (set later via scope) |
Lifetimes
- Singleton — lazy on first
get(). Same instance for every call. - Transient — always returns a fresh instance. Cannot be used with
.to(value). - Scoped — cached inside a
Scopeobject. Created viacontainer.createScope().
Scopes
const scope = container.createScope("request-1");
// Scoped registrations are cached per scope
const a = scope.get(MyScopedService);
const b = scope.get(MyScopedService);
// a === b (same scope)
// Different scope = different instance
const scope2 = container.createScope("request-2");
const c = scope2.get(MyScopedService);
// a !== cExpress Integration
import express, { Request } from "express";
import { Container, Scope } from "tiny-di.js";
import { scopedMiddleware, ScopedRequest } from "tiny-di.js/express";
const container = new Container();
container.scoped(AUTH_CTX).none();
const app = express();
app.use(scopedMiddleware(container));
app.use((req: ScopedRequest, _res, next) => {
const scope = req.scope;
scope.set(AUTH_CTX, { username: req.headers["x-user-id"] });
next();
});
app.get("/me", (req: ScopedRequest, res) => {
const ctx = req.scope.get(AUTH_CTX);
res.json(ctx);
});Circular Dependency Detection
If a cycle is detected, a descriptive error is thrown before a stack overflow can occur:
Error: Circular dependency: A => B => C => ATokens
Tokens can be a class constructor, a string, or a symbol:
container.singleton<Config>("CONFIG").to({ host: "localhost" });
container.singleton<Logger>(Symbol("logger")).to(Logger);
container.singleton(Database).toSelf();Building
pnpm buildTesting
pnpm testLicense
MIT
