diject
Lightweight TypeScript dependency injection container with decorators, scopes, metadata queries, and AsyncLocalStorage support.
Features
- Decorator-based DI:
@Service, @Repository, @Controller, @Inject, @Value, @DI, createInject
- Multiple scopes:
SINGLETON, REQUEST, TRANSIENT
- Constructor and property injection
- String/symbol/class token support
- Lifecycle hooks:
onInit, onDestroy, onBootComplete
- Metadata system with querying and phased boot
- AsyncLocalStorage integration (
AlsStore, AlsToken, createAlsToken)
Installation
npm install diject reflect-metadata
reflect-metadata is a peer dependency and must be installed by the consumer.
TypeScript setup
Enable decorator metadata in tsconfig.json:
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
Import reflect-metadata once at app startup (before using decorated classes):
import "reflect-metadata";
Quick start
import "reflect-metadata";
import { Container, Service, Inject } from "diject";
@Service()
class Logger {
log(msg: string) {
console.log(msg);
}
}
@Service()
class UserService {
@Inject()
logger!: Logger;
run() {
this.logger.log("UserService ready");
}
}
const container = new Container();
container.set([Logger, UserService]);
await container.boot();
container.get(UserService).run();
Core concepts
Register providers
container.set(MyService);
container.set([A, B, C]);
container.set("API_URL", "https://api.example.com");
container.set(Symbol.for("config"), { debug: true });
container.set(MyRepo, {
deps: [Database],
factory: (db) => new MyRepo(db)
});
Constructor injection examples
diject supports constructor injection in 3 common styles.
import "reflect-metadata";
import { Container, Service, Inject } from "diject";
@Service()
class Logger {
log(msg: string) {
console.log(msg);
}
}
@Service()
class UserRepo {}
@Service()
class UserService {
constructor(
public repo: UserRepo,
public logger: Logger
) {}
}
@Service()
class ApiClient {
constructor(@Inject("API_URL") public baseUrl: string) {}
}
class PlainHandler {
constructor(public logger: Logger) {}
}
const container = new Container();
container.set("API_URL", "https://api.example.com");
container.set([Logger, UserRepo, UserService, ApiClient]);
container.set(PlainHandler, { deps: [Logger] });
await container.boot();
const userService = container.get(UserService);
const client = container.get(ApiClient);
const plain = container.get(PlainHandler);
userService.logger.log(client.baseUrl);
plain.logger.log("plain class resolved");
Full example (constructor-based)
import "reflect-metadata";
import {
Container,
Service,
Repository,
Controller,
Inject,
Scope
} from "diject";
const API_URL = "API_URL";
@Service()
class Logger {
log(msg: string) {
console.log(`[LOG] ${msg}`);
}
}
@Service()
class Database {
connected = false;
async onInit() {
this.connected = true;
console.log("Database connected");
}
async onDestroy() {
this.connected = false;
console.log("Database disconnected");
}
query(sql: string) {
return { sql, rows: [{ id: 1, name: "Alice" }] };
}
}
@Repository()
class UserRepository {
constructor(
public db: Database,
public logger: Logger
) {}
findAll() {
this.logger.log("Loading users from database");
return this.db.query("SELECT * FROM users");
}
}
@Service({ scope: Scope.REQUEST })
class RequestContext {
createdAt = Date.now();
constructor(public logger: Logger) {
this.logger.log(`RequestContext created at ${this.createdAt}`);
}
async onDestroy() {
this.logger.log(`RequestContext destroyed at ${this.createdAt}`);
}
}
@Service()
class UserService {
constructor(
public repo: UserRepository,
public logger: Logger,
@Inject(API_URL) public apiUrl: string
) {}
async listUsers(container: Container, requestId: string) {
const ctx = await container.resolve(RequestContext, requestId);
this.logger.log(`GET ${this.apiUrl}/users (request: ${requestId})`);
return {
requestStartedAt: ctx.createdAt,
data: this.repo.findAll()
};
}
}
@Controller("/users")
class UserController {
constructor(
public service: UserService,
public logger: Logger
) {}
async index(container: Container, requestId: string) {
this.logger.log(`Handling /users for ${requestId}`);
return this.service.listUsers(container, requestId);
}
}
class UserReportJob {
constructor(
public service: UserService,
public logger: Logger
) {}
async run(container: Container) {
const result = await this.service.listUsers(container, "job-run");
this.logger.log(`Report rows: ${result.data.rows.length}`);
return result;
}
}
async function main() {
const container = new Container();
container.set(API_URL, "https://api.example.com");
container.set([
Logger,
Database,
UserRepository,
RequestContext,
UserService,
UserController
]);
container.set(UserReportJob, {
deps: [UserService, Logger]
});
await container.boot();
const controller = container.get(UserController);
const r1 = await controller.index(container, "req-1");
const r2 = await controller.index(container, "req-2");
console.log(r1.data.sql);
console.log(r2.data.sql);
const job = container.get(UserReportJob);
await job.run(container);
await container.cleanupReq("req-1");
await container.cleanupReq("req-2");
await container.cleanupReq("job-run");
await container.clear();
}
main().catch(console.error);
Expected output (example):
Database connected
[LOG] Handling /users for req-1
[LOG] RequestContext created at 1700000000000
[LOG] GET https://api.example.com/users (request: req-1)
[LOG] Loading users from database
[LOG] Handling /users for req-2
[LOG] RequestContext created at 1700000001234
[LOG] GET https://api.example.com/users (request: req-2)
[LOG] Loading users from database
SELECT * FROM users
SELECT * FROM users
[LOG] RequestContext created at 1700000002234
[LOG] GET https://api.example.com/users (request: job-run)
[LOG] Loading users from database
[LOG] Report rows: 1
[LOG] RequestContext destroyed at 1700000000000
[LOG] RequestContext destroyed at 1700000001234
[LOG] RequestContext destroyed at 1700000002234
Database disconnected
Boot and resolve
boot() initializes singleton providers
get() is synchronous and works for:
- initialized singletons
- named/symbol/raw values
- request scope only when a
requestId is provided and instance already exists
resolve() is async and should be used for request/transient providers and async factories
await container.boot();
const app = container.get(AppService);
const transient = await container.resolve(TransientService);
const reqSvc = await container.resolve(RequestScopedService, "req-1");
Scopes
import { Scope, Service } from "diject";
@Service({ scope: Scope.SINGLETON })
class SingletonSvc {}
@Service({ scope: Scope.TRANSIENT })
class TransientSvc {}
@Service({ scope: Scope.REQUEST })
class RequestSvc {}
SINGLETON: one instance per container
TRANSIENT: new instance per resolve()
REQUEST: one instance per request id
Request cleanup:
await container.cleanupReq("req-1");
Decorators
import { Controller, Repository, Service, Inject, Value, DI, Meta, createInject } from "diject";
@Service({ metadata: { layer: "domain" } })
class AuthService {}
@Repository({ database: "main", metadata: { entity: "User" } })
class UserRepo {}
@Controller({ path: "/users", metadata: { version: "v1" } })
class UserController {
@Inject()
repo!: UserRepo;
@Value("API_KEY")
apiKey?: string;
@DI()
container!: any;
}
@Meta({ critical: true })
@Service()
class CriticalService {}
Creating reusable inject decorators with createInject()
createInject() creates a reusable property decorator bound to a specific token. Useful when injecting the same dependency multiple times.
import "reflect-metadata";
import { Container, Service, createInject } from "diject";
@Service()
class Logger {
log(msg: string) {
console.log(msg);
}
}
const InjectLogger = createInject(Logger);
@Service()
class UserService {
@InjectLogger() logger!: Logger;
run() {
this.logger.log("UserService running");
}
}
@Service()
class OrderService {
@InjectLogger() logger!: Logger;
@InjectLogger({ optional: true }) debugLogger?: Logger;
process() {
this.logger.log("OrderService processing");
this.debugLogger?.log("Debug: processing started");
}
}
const container = new Container();
container.set([Logger, UserService, OrderService]);
await container.boot();
container.get(UserService).run();
container.get(OrderService).process();
You can also use tokens (strings, symbols, or injection tokens):
const API_KEY = Symbol.for("API_KEY");
const InjectApiKey = createInject(API_KEY);
@Service()
class ApiClient {
@InjectApiKey({ optional: true }) apiKey?: string;
}
Using @Meta() decorator
@Meta() lets you attach searchable metadata directly on classes. It is stackable and merged into container metadata.
import "reflect-metadata";
import { Container, Service, Meta } from "diject";
@Meta({ layer: "core", priority: 100 })
@Service()
class ConfigService {}
@Meta({ layer: "domain" })
@Meta({ tags: ["critical", "billing"] })
@Service()
class BillingService {}
const container = new Container();
container.set([ConfigService, BillingService]);
await container.boot();
const layer = container.getMeta(BillingService, "layer");
const allMeta = container.getMeta(BillingService);
const domainServices = container.find({ layer: "domain" });
const criticalServices = container.filterBy((meta) =>
Array.isArray(meta.tags) && meta.tags.includes("critical")
);
console.log(layer);
console.log(allMeta.tags);
console.log(domainServices.includes(BillingService));
console.log(criticalServices.includes(BillingService));
Metadata queries and phased boot
container.set(UserRepo, { metadata: { layer: "data", priority: 5 } });
container.set(AuthService, { metadata: { layer: "domain", priority: 10 } });
const domainTokens = container.find({ layer: "domain" });
const highPriority = container.find({ priority: (p: number) => p >= 10 });
await container.bootBy({ layer: "domain" });
await container.bootPhased("layer", ["core", "domain", "app"]);
await container.bootOrdered("priority", "desc");
Other metadata APIs:
setMeta(token, key, value) / setMeta(token, metadata)
getMeta(token, key?)
hasMeta(token, key)
deleteMeta(token, key)
clearMeta(token)
findByKey(key), groupBy(key), getValues(key), filterBy(predicate)
bulkSetMeta(tokens, metadata)
AsyncLocalStorage integration
import { Container, createAlsToken } from "diject";
const container = new Container();
const USER_TOKEN = createAlsToken<{ id: number; name: string }>("user");
await container.run({ user: { id: 1, name: "John" } }, async () => {
const user = container.get(USER_TOKEN);
console.log(user?.name);
});
Full example (ALS context + services)
import "reflect-metadata";
import { Container, Service, DI, createAlsToken } from "diject";
const REQUEST_ID = createAlsToken<string>("requestId");
const CURRENT_USER = createAlsToken<{ id: number; name: string }>("user");
@Service()
class AuditService {
@DI()
private container!: Container;
log(action: string) {
const reqId = this.container.get(REQUEST_ID) ?? "no-request";
const user = this.container.get(CURRENT_USER);
const who = user ? `${user.name}#${user.id}` : "guest";
console.log(`[${reqId}] ${who} -> ${action}`);
}
}
@Service()
class UserService {
constructor(public audit: AuditService) {}
async listUsers() {
this.audit.log("list users");
return [{ id: 1, name: "Alice" }];
}
}
async function main() {
const container = new Container();
container.set([AuditService, UserService]);
await container.boot();
const users = container.get(UserService);
await users.listUsers();
await container.run(
{
requestId: "req-A",
user: { id: 10, name: "Adam" }
},
async () => {
await users.listUsers();
await container.run(
{ user: { id: 11, name: "Bob" } },
async () => {
await users.listUsers();
}
);
await users.listUsers();
}
);
}
main().catch(console.error);
Expected output:
[no-request] guest -> list users
[req-A] Adam#10 -> list users
[req-A] Bob#11 -> list users
[req-A] Adam#10 -> list users
You can also access the raw store through container.store (AlsStore).
Lifecycle hooks
@Service()
class Database {
async onInit() {
}
async onDestroy() {
}
async onBootComplete() {
}
}
Cleanup APIs:
delete(token)
bulkRemove([TokenA, TokenB])
clear()
Global convenience API
diject exports a global singleton container and helper functions:
import { set, get, resolve, boot, has, reset } from "diject";
set("key", "value");
await boot();
const value = get("key");
For test isolation, prefer new Container() or call await reset() on the global instance.
API reference (quick)
Container
set(token, valueOrOptions) | Register class/value/token provider |
get(token, requestId?) | Sync access to initialized/available instance |
resolve(token, requestId?) | Async resolve for any provider type |
boot(services?) | Initialize singleton services |
bootBy(query) | Boot singletons matching metadata query |
bootPhased(key, phases) | Boot by ordered metadata phases |
bootOrdered(key, order?) | Boot by sorted metadata value |
cleanupReq(requestId?) | Destroy one request scope cache |
delete(token) | Remove one registration + cleanup |
clear() | Destroy and remove all providers |
find(query) | Find tokens by metadata |
run(data, fn) | Run function with ALS context |
Decorators
@Service(options?) | Register class as service |
@Repository(options?) | Register class as repository |
@Controller(options?) | Register class as controller |
@Inject(token?) | Inject constructor param or property |
@Value(token) | Optional value injection by token |
@DI() | Inject current container instance |
@Store() | Inject AlsStore instance |
@Meta(metadata) | Attach mergeable metadata |
createInject(token) | Create reusable property decorator for a token |
Suggested package.json metadata
{
"description": "Lightweight TypeScript DI container with decorators, scopes, metadata queries, and AsyncLocalStorage support.",
"keywords": [
"di",
"dependency-injection",
"ioc",
"typescript",
"decorators",
"container",
"metadata",
"async-local-storage",
"als"
]
}
License
MIT