zmdbzero-maintenance data layer
Docs Benchmarks Anti-patterns OpenAPI
Docs / Server framework

Dependency InjectionSupported

@zmdb/app provides dependency injection without emitDecoratorMetadata or reflect-metadata. Instead of reflecting constructor parameter types at runtime (the NestJS approach), you use explicit, typed tokens and a small Container. The injected field's type is inferred from its token — so you never write an as cast to satisfy the container.

Tokens#

A Token<T> carries its instance type at compile time and is identified by reference:

import { createToken } from '@zmdb/app/di';

class Logger {
  log(m: string) {
    console.log(m);
  }
}

const LoggerToken = createToken<Logger>('Logger');

The container#

import { Container } from '@zmdb/app/di';

const container = new Container();
container.register(LoggerToken, new Logger());

container.resolve(LoggerToken); // Logger  (typed — no cast)
container.has(LoggerToken); // true

// container.register(LoggerToken, 42) → compile error (42 is not a Logger)

Resolving an unregistered token throws UnresolvedTokenError:

new Container().resolve(LoggerToken); // throws UnresolvedTokenError

@Inject fields#

Declare a field and annotate it with @Inject(token). Build the class through the container to satisfy its injected fields:

import { Inject } from '@zmdb/app/di';

class UserService {
  @Inject(LoggerToken)
  logger!: Logger; // type inferred from the token — no 'as'

  greet() {
    this.logger.log('hi');
  }
}

const svc = container.build(UserService);
svc.greet();
📝 Note

Injection is resolved at container.build(...) (class-init) time and cached on the instance — not re-resolved per method call. The container is the one explicit, opt-in registry; there is no hidden global request-time state (the "current container" is set only for the duration of build and cleared in a finally).

Design notes#

see the source.)