Docs/Dependency injectionv1.0.0-rc.1

Container — API Reference

@tagna/udiot v1.0.0-rc.1


Class: Container

import { Container, globalContainer } from '@tagna/udiot';

The Container class is the core of UDIOT's DI system. It maintains a registry of providers, caches singleton and scoped instances, and detects circular dependencies during resolution.


Constructor

new Container(parent?: Container)

Creates a new container. If parent is provided the container participates in the hierarchy: any token not found locally is looked up in the parent chain.

const root  = new Container();
const child = new Container(root);

Registration Methods

All registration methods return this, so calls can be chained.

register(provider)

register(provider: AnyProvider | Function): this

The generic registration entry point. Accepts a fully specified provider object (with a provide key) or a bare class constructor (shorthand for a singleton ClassProvider).

container.register({ provide: UserService, useClass: UserService, scope: ServiceScope.SINGLETON });

// Equivalent shorthand for a class
container.register(UserService);

registerClass(token, useClass, scope?)

registerClass<T>(
  token: InjectionToken,
  useClass: new (...args: any[]) => T,
  scope?: ServiceScope
): this

Registers a class provider. The scope defaults to ServiceScope.SINGLETON when omitted.

container.registerClass(UserService, UserService);
container.registerClass(LogEntry, LogEntry, ServiceScope.TRANSIENT);
container.registerClass(RequestContext, RequestContext, ServiceScope.SCOPED);

registerValue(token, value)

registerValue<T>(token: InjectionToken, value: T): this

Registers a constant value. No scope applies — the same value is always returned.

container.registerValue('DB_URL', 'postgres://localhost/app');
container.registerValue('MAX_RETRIES', 3);
container.registerValue('APP_CONFIG', { debug: false, region: 'us-east-1' });

registerFactory(token, factory, inject?)

registerFactory<T>(
  token: InjectionToken,
  factory: (...args: any[]) => T,
  inject?: InjectionToken[]
): this

Registers a factory function. Tokens listed in inject are resolved first and passed as positional arguments to factory. Like classes, factories default to SINGLETON; pass a provider object with scope to change that.

container.registerFactory(
  ConnectionPool,
  (config: DbConfig) => new ConnectionPool(config.url, { max: config.poolSize }),
  ['DB_CONFIG']
);

registerMany(providers[])

registerMany(providers: any[]): this

Bulk registration. Equivalent to calling register() for each entry. Useful when bootstrapping a module.

container.registerMany([
  { provide: 'DB_URL', useValue: process.env.DATABASE_URL },
  { provide: DatabaseService, useClass: DatabaseService },
  { provide: UserRepository, useClass: UserRepository, scope: ServiceScope.SINGLETON },
]);

Resolution Methods

resolve<T>(token)

resolve<T>(token: InjectionToken): T

Resolves a token and returns the corresponding instance or value. Throws ProviderNotFoundError if the token is not registered in this container or any ancestor. Throws CircularDependencyError if resolution would create an infinite cycle.

const svc = container.resolve<UserService>(UserService);
const url = container.resolve<string>('DB_URL');

Auto-registration: if token is a class decorated with @Injectable and is not yet registered, the container registers it automatically on first resolve().

Providers are looked up through the parent chain first, so a child never shadows a parent's provider or override. A SINGLETON is built once in the container that owns it and shared by every child scope, unless something in its dependency graph only exists in a narrower scope (a SCOPED provider, or a value registered in the child such as the request); then it is built once per scope.

resolveAsync<T>(token)

resolveAsync<T>(token: InjectionToken): Promise<T>

Use when a provider or one of its dependencies is an async factory. Async dependencies are awaited before constructors run, and settled singletons replace their promises in the cache, so later resolve() calls get the value.

container.registerFactory(DB, async () => connect(process.env.DATABASE_URL));
const repo = await container.resolveAsync(UserRepository); // receives the connected DB

resolveOptional<T>(token) (pattern)

The container does not expose resolveOptional as a built-in method. The idiomatic pattern for optional resolution is:

function resolveOptional<T>(container: Container, token: InjectionToken): T | undefined {
  try {
    return container.resolve<T>(token);
  } catch {
    return undefined;
  }
}

Alternatively, decorate the constructor parameter with @Optional() (see decorators.md).


has(token)

has(token: InjectionToken): boolean

Returns true if the token is registered in this container or any ancestor in the parent chain.

if (!container.has(CacheService)) {
  container.registerClass(CacheService, CacheService);
}

Scoping Methods

createChild()

createChild(): Container

Creates a child container that inherits this container's providers. SCOPED providers from the parent are automatically copied into the child so each child gets its own instance of scoped services.

const moduleContainer = root.createChild();
moduleContainer.registerClass(UserService, UserService);

// moduleContainer can resolve UserService (local) and DatabaseService (inherited from root)

Request-Scoped Container Pattern

The canonical pattern for HTTP request isolation:

// At app startup
const root = new Container();
root.registerClass(DatabaseService, DatabaseService);

// Per request (e.g., in Express middleware)
app.use((req, res, next) => {
  const requestContainer = root.createChild();
  requestContainer.registerValue('REQUEST_ID', crypto.randomUUID());
  requestContainer.registerClass(RequestContext, RequestContext, ServiceScope.SCOPED);

  req.container = requestContainer;
  next();
});

Utility Methods

getTokens()

getTokens(): InjectionToken[]

Returns an array of all tokens registered in this container only (does not include ancestors).

clear()

clear(): void

Clears the instance cache. Singleton and scoped instances will be re-created on the next resolve(). Useful in test afterEach hooks.

dispose()

dispose(): Promise<void>

Disposes every instance this container built, newest first, by calling its onDestroy(), dispose(), [Symbol.asyncDispose]() or [Symbol.dispose](). Values registered with registerValue() and instances owned by a parent are left alone. createRequestScopeMiddleware() and the tenant middleware dispose their child containers when the response ends.


Global Container

import { globalContainer } from '@tagna/udiot';

globalContainer is a pre-created Container instance exported from the package. It acts as the application root. Services decorated with @Injectable({ providedIn: 'root' }) are automatically registered here.

import { globalContainer, inject } from '@tagna/udiot';

globalContainer.registerClass(LoggerService, LoggerService);

// Convenience wrapper for globalContainer.resolve()
const logger = inject<LoggerService>(LoggerService);

Circular Dependency Detection

The container maintains a resolutionStack array during every resolve() call. Before resolving a token it checks whether that token is already in the stack. If it is, a CircularDependencyError is thrown immediately, displaying the full dependency chain.

CircularDependencyError: Circular dependency detected: UserService -> OrderService -> UserService

Example that triggers the error:

@Injectable()
class ServiceA {
  constructor(private b: ServiceB) {}
}

@Injectable()
class ServiceB {
  constructor(private a: ServiceA) {} // A → B → A: cycle
}

container.resolve(ServiceA); // throws CircularDependencyError

To break cycles, consider:

  • Injecting a factory function (() => ServiceB) instead of the instance.
  • Restructuring to extract shared logic into a third service that neither A nor B depends on.
  • Using @InjectProperty (property injection) so that the circular class can be instantiated before the cyclic dependency is resolved.

Examples

Root Container + Child Module Container

import { Container, ServiceScope } from '@tagna/udiot';

// Infrastructure layer
const root = new Container();
root.registerValue('DB_URL', 'postgres://localhost/myapp');
root.registerClass(DatabaseService, DatabaseService);

// User module
const userModule = root.createChild();
userModule.registerClass(UserRepository, UserRepository);
userModule.registerClass(UserService, UserService);

// Resolve — UserService gets UserRepository and DatabaseService automatically
const userService = userModule.resolve<UserService>(UserService);

Overriding a Parent Provider in a Child Container

root.registerValue('LOG_LEVEL', 'info');

const testContainer = root.createChild();
testContainer.registerValue('LOG_LEVEL', 'silent'); // override for tests

testContainer.resolve<string>('LOG_LEVEL'); // 'silent'
root.resolve<string>('LOG_LEVEL');          // 'info'

Request-Scoped Container Full Example

import { Container, ServiceScope } from '@tagna/udiot';

@Injectable({ scope: ServiceScope.SINGLETON })
class DatabaseService { /* ... */ }

@Injectable({ scope: ServiceScope.SCOPED })
class UnitOfWork {
  constructor(private db: DatabaseService) {}
  commit(): void { /* ... */ }
}

const appContainer = new Container();
appContainer.registerClass(DatabaseService, DatabaseService);
appContainer.registerClass(UnitOfWork, UnitOfWork, ServiceScope.SCOPED);

function handleRequest(): void {
  const requestContainer = appContainer.createChild();

  // Each request gets its own UnitOfWork
  const uow = requestContainer.resolve<UnitOfWork>(UnitOfWork);
  // ... process request ...
  uow.commit();
  // requestContainer goes out of scope; UnitOfWork instance is garbage collected
}
From the canonical UDIOT public documentation.Try it in the playground ↗