DEV Community

Hossam Hamdy
Hossam Hamdy

Posted on

I built Injectus, a decorator-free DI container for Node, looking for early users/feedback

I've been building a dependency injection container for Node/TypeScript called Injectus, and I'm at the point where I'd rather have a handful of people actually try it and poke holes in it than keep polishing alone. It's 0.2.x not "adopt this in prod tomorrow" young, but the core is solid and I want real usage before I push it further.

What it is: zero-dependency, decorator-free IoC container. No reflect-metadata, no emitDecoratorMetadata, no build config wrestling.

import { Injector, inject, InjectionToken } from "injectus";

const DB_URL = new InjectionToken<string>("DB_URL");

class Database {
  url = inject(DB_URL);
}

class UserService {
  db = inject(Database);
  findAll() {
    return this.db.url;
  }
}

const injector = Injector.create({
  providers: [
    { provide: DB_URL, useValue: "postgres://localhost/app" },
    Database,
    UserService,
  ],
});

injector.resolve(UserService).findAll();
await injector.dispose();
Enter fullscreen mode Exit fullscreen mode

Why I built it: I kept hitting the same friction with existing containers - decorators requiring specific tsconfig/build setups, string or symbol tokens silently colliding, and singleton/scoped lifetime mismatches only surfacing as bugs in production. Injectus's actual differentiators:

  • Decorator-free - inject() works in field initializers and factory bodies, no metadata reflection required.
  • Identity-based tokens - classes and InjectionToken instances are the key, not their string description. Two tokens named "Logger" can't collide.
  • Captive dependency detection - resolving a Singleton that depends on a Scoped binding throws CaptiveDependencyError at resolve time, not silently in prod.
  • Deterministic disposal - LIFO teardown via Symbol.asyncDispose, multiple failing disposers collected into one AggregateError, full dependency path exposed root-to-leaf on CircularDependencyError/CaptiveDependencyError.

It also benchmarks well against the two closest points of comparison - awilix (decorator-free) and tsyringe (decorator-based) - resolving the same dependency-graph shape in each case:

What's honestly not done yet, so you know what you're getting into:

  • No module system yet - (flat provider arrays only, for now)
  • No multi-injection - registering the same token twice currently shadows (last-write-wins)
  • No async init hooks / lifecycle ordering yet
  • No framework adapters (Express example exists, others don't)

Top comments (3)

Collapse
 
hossam_hamdy_a981027811d6 profile image
Hossam Hamdy

Here is the GitHub repository link for anyone interested in inspecting the code or trying it out:

github.com/hossam7amdy/injectus

Collapse
 
nazar-boyko profile image
Nazar Boyko

You asked for holes, so here's the first place I'd push. With inject() running inside a field initializer and no decorators or metadata, how does it know which injector it belongs to at that moment? My guess is there's an ambient "current resolution" context alive while injector.resolve() runs. If that's the trick, what happens when someone builds the class directly with new UserService() outside a resolve call, or lazily inside an async callback after that context has unwound? That edge is where designs like this usually get interesting, so it'd be worth a line in the README so nobody trips over it. The captive dependency check throwing at resolve time is a really nice touch, by the way.

Collapse
 
hossam_hamdy_a981027811d6 profile image
Hossam Hamdy

Good catch, you nailed the mechanism. There's a module-global currentContext that resolve() sets around the factory/field initializer and restores in finally, same trick Angular's inject() uses, borrowed directly from there.

Direct new UserService() outside resolve(), or inject() after await/setTimeout, throws InjectionContextError.

For manual construction, the same idea as constructor params:

constructor(
  private logger = inject(Logger),
  private userRepo = inject(UserRepository)) {}
Enter fullscreen mode Exit fullscreen mode