
Product
Introducing Repository Access Permissions and Custom Roles
Socket now supports Custom Roles and Repository Access Permissions so organizations can control who can access specific repositories and actions.
Lightweight TypeScript DI container with decorators, scopes, metadata queries, and AsyncLocalStorage support.
Lightweight TypeScript dependency injection container with decorators, scopes, metadata queries, and AsyncLocalStorage support.
@Service, @Repository, @Controller, @Inject, @Value, @DI, createInjectSINGLETON, REQUEST, TRANSIENTonInit, onDestroy, onBootCompleteAlsStore, AlsToken, createAlsToken)npm install diject reflect-metadata
reflect-metadata is a peer dependency and must be installed by the consumer.
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";
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();
container.set(MyService); // class
container.set([A, B, C]); // batch
container.set("API_URL", "https://api.example.com"); // named token
container.set(Symbol.for("config"), { debug: true }); // symbol token
container.set(MyRepo, {
deps: [Database],
factory: (db) => new MyRepo(db)
});
diject supports constructor injection in 3 common styles.
import "reflect-metadata";
import { Container, Service, Inject } from "diject";
// 1) Type-based constructor injection (most common)
@Service()
class Logger {
log(msg: string) {
console.log(msg);
}
}
@Service()
class UserRepo {}
@Service()
class UserService {
constructor(
public repo: UserRepo,
public logger: Logger
) {}
}
// 2) Token-based constructor injection with @Inject(token)
@Service()
class ApiClient {
constructor(@Inject("API_URL") public baseUrl: string) {}
}
// 3) Plain class (no decorator) using explicit deps
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");
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();
// Named token
container.set(API_URL, "https://api.example.com");
// Decorated providers
container.set([
Logger,
Database,
UserRepository,
RequestContext,
UserService,
UserController
]);
// Plain class with explicit constructor deps
container.set(UserReportJob, {
deps: [UserService, Logger]
});
// Initialize singletons
await container.boot();
// Simulate HTTP requests
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);
// Run plain class workflow
const job = container.get(UserReportJob);
await job.run(container);
// Cleanup request-scoped instances
await container.cleanupReq("req-1");
await container.cleanupReq("req-2");
await container.cleanupReq("job-run");
// Cleanup singletons and call onDestroy hooks
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() initializes singleton providersget() is synchronous and works for:
requestId is provided and instance already existsresolve() is async and should be used for request/transient providers and async factoriesawait container.boot();
const app = container.get(AppService);
const transient = await container.resolve(TransientService);
const reqSvc = await container.resolve(RequestScopedService, "req-1");
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 containerTRANSIENT: new instance per resolve()REQUEST: one instance per request idRequest cleanup:
await container.cleanupReq("req-1");
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; // optional by default
@DI()
container!: any;
}
@Meta({ critical: true })
@Service()
class CriticalService {}
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);
}
}
// Create a reusable decorator for Logger
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;
}
@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();
// Read metadata
const layer = container.getMeta(BillingService, "layer");
const allMeta = container.getMeta(BillingService);
// Query by metadata
const domainServices = container.find({ layer: "domain" });
const criticalServices = container.filterBy((meta) =>
Array.isArray(meta.tags) && meta.tags.includes("critical")
);
console.log(layer); // domain
console.log(allMeta.tags); // ["critical", "billing"]
console.log(domainServices.includes(BillingService)); // true
console.log(criticalServices.includes(BillingService)); // true
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)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); // John
});
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);
// No ALS context yet
await users.listUsers();
// Request A context
await container.run(
{
requestId: "req-A",
user: { id: 10, name: "Adam" }
},
async () => {
await users.listUsers();
// Nested context inherits parent, then overrides user
await container.run(
{ user: { id: 11, name: "Bob" } },
async () => {
await users.listUsers();
}
);
// Back to parent context (Adam)
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).
@Service()
class Database {
async onInit() {
// connect
}
async onDestroy() {
// disconnect
}
async onBootComplete() {
// called after boot cycle completes
}
}
Cleanup APIs:
delete(token)bulkRemove([TokenA, TokenB])clear()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.
| Method | Purpose |
|---|---|
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 |
| Decorator | Purpose |
|---|---|
@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 |
{
"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"
]
}
MIT
FAQs
Lightweight TypeScript DI container with decorators, scopes, metadata queries, and AsyncLocalStorage support.
The npm package diject receives a total of 14 weekly downloads. As such, diject popularity was classified as not popular.
We found that diject demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Product
Socket now supports Custom Roles and Repository Access Permissions so organizations can control who can access specific repositories and actions.

Product
Socket MCP now lets AI assistants review org alerts, investigate threats using the Socket threat feed, and inspect package files in addition to dependency scoring.

Product
Socket Firewall blocks malicious VS Code and Open VSX extensions before install, protecting developers from compromised editor marketplaces.