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
}