@napp/di
v1.0.10
Published
Browser and Nodejs dependency inject powerfull library
Maintainers
Readme
@napp/di
A lightweight, tree-based Dependency Injection (DI) container for TypeScript.
- No decorators
- No
reflect-metadata - Fully type-safe constructor injection
- Scoped / Singleton / Transient lifetimes
- Explicit container tree
- Zero magic, predictable behavior
Install
npm install @napp/diWhat is this DI?
@napp/di is a runtime-explicit DI system.
Instead of relying on decorators, metadata, or global state, you:
- explicitly register providers
- explicitly resolve dependencies
- explicitly manage container scopes
This makes behavior:
- predictable
- debuggable
- safe for both backend & frontend apps
Basic usage
import { Container, INJECT } from "@napp/di";
class ServiceA {}
class ServiceB {
static [INJECT] = [ServiceA] as const;
constructor(a: ServiceA) {}
}
const root = new Container("root").asClass(ServiceA).asClass(ServiceB);
const b = root.resolve(ServiceB);No decorators, no reflect-metadata
This DI system does not use:
- decorators
- emitDecoratorMetadata
- reflect-metadata
Injection is defined explicitly using a static INJECT array.
Why?
- Faster startup
- Smaller bundle
- No hidden runtime magic
- Works everywhere (Node, Browser, Edge)
Container Tree
Containers form a tree, not a global singleton.
const root = new Container("root");
const requestScope = root.child("request");Resolution walks upward through the tree.
Lifetimes
-SINGLETON
One instance for:
- the container where it was registered
- all of its children
.asClass(ServiceA, Lifetime.SINGLETON)root
├─ child1
└─ child2Same instance everywhere
-SCOPED (default)
- One instance per resolving container
- Child containers get their own instance
.asClass(ServiceA, Lifetime.SCOPED)root → instance A1
├─ child → instance A2-TRANSIENT
New instance every resolve
.asClass(ServiceA, Lifetime.TRANSIENT)Providers
asClass Register a class as a provider.
container.asClass(UserService);
container.asClass(UserService, Lifetime.SINGLETON);asFactory Use a factory function.
container.asFactory(
CONFIG,
() => ({ apiUrl: "https://api.example.com" }),
Lifetime.SINGLETON
);Factory receives the current container.
asValue Register a constant value.
container.asValue(CONFIG, { debug: true });asAlias Alias one token to another.
container.asAlias(Logger, ConsoleLogger);Useful for:
- swapping implementations
- testing
- environment overrides
Token
const CONFIG = Token.create<string>("config");Notes
- Token.create(string) generates a unique symbol key
- Token keys never collide, even if names match
- Classes can be used directly as DI tokens
static [TOKEN]is NOT required
Optional: static [TOKEN] for better debugging
Defining a static [TOKEN] on a class is optional.
It does not change DI behavior, but it makes debug output and error messages easier to read.
Module (Lightweight grouping)
Modules are only for grouping & debugging.
They do not create scopes or affect resolution.
const UserModule = {
name: "user",
providers: [asClass(UserService), asClass(UserRepository)],
};
container.registryModule(UserModule);Why modules exist here?
- Group providers logically
- Show provider origin in debug output
- No hidden container placement logic
Debugging
console.log(container.debug());Shows:
- container tree
- providers
- lifetime
- module origin
- override state
Perfect for dev & diagnostics.
Destroy & Disposal
Containers support explicit lifecycle cleanup.
If an instance implements:
interface Disposable {
dispose(): void;
}Then:
container.destroy();Will:
- Destroy child containers first
- Call dispose() on owned instances
- Clear providers & caches
Important
SINGLETONinstances are owned by the container where registeredSCOPEDinstances are owned by the resolving container
No accidental cross-module destruction.
Type-safe Injection (Compile-time)
You can optionally enforce constructor ↔ inject matching:
static[INJECT] = defineInject(ServiceB, [ServiceA, CONFIG] as const);This guarantees at compile time:
- correct order
- correct count
- correct types
Zero runtime cost.
