Skip to content
Buy me a coffee

Introduction

@nodelibraries/ioc builds your object graph for you. You register each service once, with a lifetime and the tokens of its dependencies, and ask the container for what you need. It creates instances in the right order, shares them according to their lifetime and disposes them when a scope ends.

import {
class ServiceCollection

Collects service registrations, then builds a

ServiceProvider

. Modeled on .NET's IServiceCollection.

@example

const services = new ServiceCollection();
services.addSingleton(Logger, ConsoleLogger);
services.addScoped(UserService, [Logger]);
const provider = services.buildServiceProvider({ validateScopes: true });

ServiceCollection
,
function createToken<T>(name: string): InjectionToken<T>

Creates a typed token.

@example

interface Logger { log(msg: string): void }
const Logger = createToken<Logger>('Logger');
services.addSingleton(Logger, ConsoleLogger);
const logger = await provider.getRequiredService(Logger); // typed as Logger

createToken
} from '@nodelibraries/ioc';
interface
interface Logger
Logger
{
Logger.log(message: string): void
log
(
message: string
message
: string): void;
}
const
const Logger: InjectionToken<Logger>
Logger
=
createToken<Logger>(name: string): InjectionToken<Logger>

Creates a typed token.

@example

interface Logger { log(msg: string): void }
const Logger = createToken<Logger>('Logger');
services.addSingleton(Logger, ConsoleLogger);
const logger = await provider.getRequiredService(Logger); // typed as Logger

createToken
<
interface Logger
Logger
>('Logger');
class
class ConsoleLogger
ConsoleLogger
implements
interface Logger
Logger
{
ConsoleLogger.log(message: string): void
log
(
message: string
message
: string) {
var console: Console

The console module provides a simple debugging console that is similar to the JavaScript console mechanism provided by web browsers.

The module exports two specific components:

  • A Console class with methods such as console.log(), console.error() and console.warn() that can be used to write to any Node.js stream.
  • A global console instance configured to write to process.stdout and process.stderr. The global console can be used without importing the node:console module.

Warning: The global console object's methods are neither consistently synchronous like the browser APIs they resemble, nor are they consistently asynchronous like all other Node.js streams. See the note on process I/O for more information.

Example using the global console:

console.log('hello world');
// Prints: hello world, to stdout
console.log('hello %s', 'world');
// Prints: hello world, to stdout
console.error(new Error('Whoops, something bad happened'));
// Prints error message and stack trace to stderr:
// Error: Whoops, something bad happened
// at [eval]:5:15
// at Script.runInThisContext (node:vm:132:18)
// at Object.runInThisContext (node:vm:309:38)
// at node:internal/process/execution:77:19
// at [eval]-wrapper:6:22
// at evalScript (node:internal/process/execution:76:60)
// at node:internal/main/eval_string:23:3
const name = 'Will Robinson';
console.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to stderr

Example using the Console class:

const out = getStreamSomehow();
const err = getStreamSomehow();
const myConsole = new console.Console(out, err);
myConsole.log('hello world');
// Prints: hello world, to out
myConsole.log('hello %s', 'world');
// Prints: hello world, to out
myConsole.error(new Error('Whoops, something bad happened'));
// Prints: [Error: Whoops, something bad happened], to err
const name = 'Will Robinson';
myConsole.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to err

@see ― source

console
.
Console.log(message?: any, ...optionalParams: any[]): void (+1 overload)

Prints to stdout with newline. Multiple arguments can be passed, with the first used as the primary message and all additional used as substitution values similar to printf(3) (the arguments are all passed to util.format()).

const count = 5;
console.log('count: %d', count);
// Prints: count: 5, to stdout
console.log('count:', count);
// Prints: count: 5, to stdout

See util.format() for more information.

@since ― v0.1.100

log
(
message: string
message
);
}
}
class
class UserService
UserService
{
constructor(private readonly
UserService.logger: Logger
logger
:
interface Logger
Logger
) {}
UserService.list(): string[]
list
() {
this.
UserService.logger: Logger
logger
.
Logger.log(message: string): void
log
('listing users');
return ['Ada', 'Linus'];
}
}
const
const services: ServiceCollection
services
= new
new ServiceCollection(): ServiceCollection

Collects service registrations, then builds a

ServiceProvider

. Modeled on .NET's IServiceCollection.

@example

const services = new ServiceCollection();
services.addSingleton(Logger, ConsoleLogger);
services.addScoped(UserService, [Logger]);
const provider = services.buildServiceProvider({ validateScopes: true });

ServiceCollection
();
const services: ServiceCollection
services
.
ServiceCollection.addSingleton<Logger>(token: Token<Logger>, implementation: Newable<NoInfer<Logger>, []>): ServiceCollection (+4 overloads)

Registers a singleton: one instance for the root provider and all its scopes.

  • addSingleton(Class) / addSingleton(Class, [deps]): the class is its own token.
  • addSingleton(token, Implementation, [deps]): deps are checked against the constructor's parameters.
  • addSingleton(token, (provider) => instance): a factory, may be async.

addSingleton
(
const Logger: InjectionToken<Logger>
Logger
,
class ConsoleLogger
ConsoleLogger
);
const services: ServiceCollection
services
.
ServiceCollection.addScoped<UserService, [logger: Logger]>(token: Newable<UserService, [logger: Logger]>, dependencies: readonly [logger: Token<Logger>]): ServiceCollection (+4 overloads)

Registers a scoped service: one instance per scope. Same forms as

addSingleton

.

addScoped
(
class UserService
UserService
, [
const Logger: InjectionToken<Logger>
Logger
]);
const
const provider: ServiceProvider
provider
=
const services: ServiceCollection
services
.
ServiceCollection.buildServiceProvider(options?: BuildOptions): ServiceProvider

Builds the provider. Registrations are copied, so changing this collection afterwards does not affect providers already built.

buildServiceProvider
({
BuildOptions.validateScopes?: boolean | undefined

Throw a ScopeError when a scoped service is resolved from the root provider or captured by a singleton. Recommended in development.

validateScopes
: true });
await using
await using scope: ServiceProvider
scope
=
const provider: ServiceProvider
provider
.
ServiceProvider.createScope(): ServiceProvider

Creates a scope: scoped services resolve once per scope. Dispose it when done (or use await using).

createScope
();
const
const users: UserService
users
= await
await using scope: ServiceProvider
scope
.
ServiceProvider.getRequiredService<UserService>(token: Token<UserService>): Promise<UserService>

Resolves token, throwing ServiceNotFoundError if it is not registered.

getRequiredService
(
class UserService
UserService
);
const users: UserService
users
.
UserService.list(): string[]
list
();

UserService needs a Logger, so it is registered with [Logger]: one token per constructor parameter, in order. The compiler checks typed tokens in that list against the constructor’s parameter types, and the container checks the count at registration.

Need Feature
Share or isolate instances Singleton, scoped and transient lifetimes, scopes
Interfaces as dependencies Typed tokens with createToken<T>(), abstract classes, strings, symbols
Catch wiring mistakes early Type-checked dependencies, validateScopes and validateOnBuild
Clean shutdown onInit, onDestroy, Symbol.dispose and await using
Real-world graphs Circular dependencies, factories, keyed services, multiple implementations
Errors you can handle Typed errors with the resolution path in the message

No decorators, no reflect-metadata, no compiler plugins. Classes stay plain classes. The package has zero dependencies, ships ESM and CommonJS builds with type declarations, and runs on Node.js 20 or later (and in browsers: this site runs it in its playground).

  • Your app has more than a handful of services and wiring them by hand in main.ts is getting long.
  • You need per-request state (a request ID, a database transaction, the current user) without threading it through every call.
  • You want tests to swap one dependency (a clock, a mail sender) without touching the code under test.
  • You know .NET’s IServiceCollection and want the same model in TypeScript.

If your app is a single file with three objects, new is fine. A container pays off when the graph grows.