🚀 Socket Launch Week Day 5:Introducing Repository Access Permissions and Custom Roles.Learn more
Sign In

diject

Package Overview
Dependencies
Maintainers
1
Versions
12
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

diject

Lightweight TypeScript DI container with decorators, scopes, metadata queries, and AsyncLocalStorage support.

latest
npmnpm
Version
0.1.5
Version published
Weekly downloads
14
-91.25%
Maintainers
1
Weekly downloads
 
Created
Source

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); // 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)
});

Constructor injection examples

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");

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();

  // 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 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; // optional by default

  @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);
  }
}

// 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;
}

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();

// 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

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); // John
});

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);

  // 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).

Lifecycle hooks

@Service()
class Database {
  async onInit() {
    // connect
  }

  async onDestroy() {
    // disconnect
  }

  async onBootComplete() {
    // called after boot cycle completes
  }
}

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

MethodPurpose
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

DecoratorPurpose
@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

Keywords

di

FAQs

Package last updated on 18 May 2026

Did you know?

Socket

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.

Install

Related posts