DI Container & Scopes
Instance lifecycle scopes and the container API — register, resolve, and manage dependencies.
Continued from Dependency Injection — scopes, the container API, and operational notes.
Scopes
Three lifecycle scopes control how the container manages instances:
One instance shared across all resolve() calls:
@Service()
class ConfigService {
readonly startTime = Date.now();
}
const a = container.resolve(ConfigService);
const b = container.resolve(ConfigService);
console.log(a === b); // trueNew instance for each resolve() call:
@Service({ scope: 'transient' })
class RequestContext {
readonly id = Math.random();
}
const a = container.resolve(RequestContext);
const b = container.resolve(RequestContext);
console.log(a === b); // falseOne instance per request, shared within that request. Backed by tsyringe's
ContainerScoped lifecycle and a per-request child container (createChild()):
@Service({ scope: 'request' })
class RequestId {
readonly id = crypto.randomUUID();
}
const child = container.createChild(); // one child per request
const a = child.resolve(RequestId);
const b = child.resolve(RequestId);
console.log(a === b); // true — shared within the requestRequest scope through controllers
With registerControllers, request scope is automatic. A controller (or any
service in its dependency graph) that uses scope: 'request' is resolved from
a fresh per-request child on each request; a purely-singleton controller keeps
the memoized fast path with zero added per-request cost. See the
class-based guide.
Container API
The global container and containers created with createContainer() implement Container.
container.register(token, provider)
Register a dependency using one of three provider types:
// Class provider
container.register(UserService, { useClass: UserServiceImpl });
// Value provider
container.register('CONFIG', { useValue: { port: 8080 } });
// Factory provider — receives the container for nested resolution
container.register('DB', {
useFactory: (c) => createConnection(c.resolve<Config>('CONFIG')),
});Provider Types
| Property | Type | Description |
|---|---|---|
useClass | Constructor<T> | Instantiate this class when the token is resolved |
useValue | T | Return this exact value when the token is resolved |
useFactory | (container: Container) => T | Call this factory function when the token is resolved |
container.resolve(token)
Resolve a dependency. Throws DependencyResolutionError if the token is not registered and cannot be auto-resolved.
const service = container.resolve(UserService);
const config = container.resolve<Config>('CONFIG');container.resolveAll(token)
Resolve all instances registered under a token. Returns an empty array if none are registered.
container.register('Handler', { useValue: handlerA });
container.register('Handler', { useValue: handlerB });
const handlers = container.resolveAll<Handler>('Handler'); // [handlerA, handlerB]container.isRegistered(token)
Check whether a token has been registered.
container.isRegistered(UserService); // true or falsecontainer.clearInstances()
Clear cached singleton instances. Registrations remain — the next resolve() creates fresh instances.
beforeEach(() => {
container.clearInstances();
});container.reset()
Reset the container completely, removing all registrations and instances.
container.createChild()
Create a child container. The child inherits parent registrations but can override them independently.
const child = container.createChild();
child.register('CONFIG', { useValue: { port: 4000 } });
// Parent container still has original CONFIGcreateContainer()
Create a new isolated container with no inherited registrations. Internally creates a child of the global tsyringe container and immediately resets it.
import { createContainer } from 'nextrush/class';
const testContainer = createContainer();
testContainer.register(UserRepository, { useClass: MockUserRepository });Performance Notes
- Singleton resolution pays the construction cost once. Subsequent calls return the cached instance with negligible overhead.
- Transient resolution constructs a new instance on every call. Avoid transient scope for services resolved in hot paths (e.g., per-request middleware).
- The
delay()wrapper adds one level of indirection per resolution. Use it only for genuine circular dependencies.
Security Considerations
- The container holds application-wide state. Do not expose it to untrusted code or serialize it.
- Avoid storing secrets (API keys, credentials) as plain string tokens in client-accessible bundles. Use environment variables and resolve configuration at startup.
- Factory providers receive the container instance. Ensure factory functions do not leak the container reference outside their scope.
When Not To Use
- Pure functional applications with no class-based services. Manual wiring or factory functions are sufficient.
- Very small applications (under 3–4 services) where the overhead of decorators and metadata outweighs the wiring pain.
- Serverless functions that create a fresh process per invocation — singleton caching provides no benefit.
Next Steps
- Dependency Injection — Decorators and configuration
- DI errors & utilities — Error classes, metadata inspection, TypeScript exports
- @nextrush/class overview — How the class runtime fits together