Skip to content
Buy me a coffee

Quick start

Five steps, each building on the last. Every snippet on this page is type-checked against the library.

Services are ordinary classes that take their dependencies in the constructor. Nothing about them mentions the container.

services.ts
export interface
interface Logger
Logger
{
Logger.log(message: string): void
log
(
message: string
message
: string): void;
}
export 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
(`[app] ${
message: string
message
}`);
}
}
export class
class UserRepository
UserRepository
{
constructor(private readonly
UserRepository.logger: Logger
logger
:
interface Logger
Logger
) {}
async
UserRepository.findAll(): Promise<{
id: number;
name: string;
}[]>
findAll
() {
this.
UserRepository.logger: Logger
logger
.
Logger.log(message: string): void
log
('SELECT * FROM users');
return [{
id: number
id
: 1,
name: string
name
: 'Ada' }];
}
}
export class
class UserService
UserService
{
constructor(
private readonly
UserService.repo: UserRepository
repo
:
class UserRepository
UserRepository
,
private readonly
UserService.logger: Logger
logger
:
interface Logger
Logger
,
) {}
async
UserService.names(): Promise<string[]>
names
() {
this.
UserService.logger: Logger
logger
.
Logger.log(message: string): void
log
('UserService.names');
return (await this.
UserService.repo: UserRepository
repo
.
UserRepository.findAll(): Promise<{
id: number;
name: string;
}[]>
findAll
()).
Array<{ id: number; name: string; }>.map<string>(callbackfn: (value: {
id: number;
name: string;
}, index: number, array: {
id: number;
name: string;
}[]) => string, thisArg?: any): string[]

Calls a defined callback function on each element of an array, and returns an array that contains the results.

@param ― callbackfn A function that accepts up to three arguments. The map method calls the callbackfn function one time for each element in the array.

@param ― thisArg An object to which the this keyword can refer in the callbackfn function. If thisArg is omitted, undefined is used as the this value.

map
((
u: {
id: number;
name: string;
}
u
) =>
u: {
id: number;
name: string;
}
u
.
name: string
name
);
}
}

Interfaces don’t exist at runtime, so the container can’t use Logger itself as a key. A typed token stands in for it. Classes are their own tokens.

tokens.ts
import {
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';
export 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');

A value and a type may share a name in TypeScript, so Logger can be both the interface and its token. More in Tokens.

container.ts
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
} from '@nodelibraries/ioc';
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
()
.
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
) // token, implementation
.
ServiceCollection.addSingleton<UserRepository, [logger: Logger]>(token: Newable<UserRepository, [logger: Logger]>, dependencies: readonly [logger: Token<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
(
class UserRepository
UserRepository
, [
const Logger: InjectionToken<Logger>
Logger
]) // class as its own token, [deps]
.
ServiceCollection.addTransient<UserService, [repo: UserRepository, logger: Logger]>(token: Newable<UserService, [repo: UserRepository, logger: Logger]>, dependencies: readonly [repo: Token<UserRepository>, logger: Token<Logger>]): ServiceCollection (+4 overloads)

Registers a transient service: a new instance on every resolution. Same forms as

addSingleton

.

addTransient
(
class UserService
UserService
, [
class UserRepository
UserRepository
,
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
();
const users = await
const provider: ServiceProvider
provider
.
ServiceProvider.getRequiredService<UserService>(token: Token<UserService>): Promise<UserService>

Resolves token, throwing ServiceNotFoundError if it is not registered.

getRequiredService
(
class UserService
UserService
);
const users: UserService
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
(await
const users: UserService
users
.
UserService.names(): Promise<string[]>
names
()); // ['Ada']

The dependency array lists one token per constructor parameter, in order. Put the Logger token first and the compiler rejects it, because a Logger is not a UserRepository. See Dependencies for what the check covers.

Resolution is always async: factories may be async and onInit hooks may await, so every get* method returns a promise.

A scoped service lives once per scope. In a server, one scope per request gives every request its own unit of work (a transaction, a request ID) while singletons stay shared.

server.ts
class
class RequestContext
RequestContext
{
readonly
RequestContext.id: `${string}-${string}-${string}-${string}-${string}`
id
=
var crypto: Crypto
crypto
.
Crypto.randomUUID(): `${string}-${string}-${string}-${string}-${string}`

The randomUUID() method of the Crypto interface is used to generate a v4 UUID using a cryptographically secure random number generator. Available only in secure contexts.

MDN Reference

randomUUID
();
}
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
().
ServiceCollection.addScoped<RequestContext>(token: Newable<RequestContext, []>): ServiceCollection (+4 overloads)

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

addSingleton

.

addScoped
(
class RequestContext
RequestContext
);
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 });
async function
function onRequest(): Promise<void>
onRequest
() {
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 ctx: RequestContext
ctx
= await
await using scope: ServiceProvider
scope
.
ServiceProvider.getRequiredService<RequestContext>(token: Token<RequestContext>): Promise<RequestContext>

Resolves token, throwing ServiceNotFoundError if it is not registered.

getRequiredService
(
class RequestContext
RequestContext
);
await
function handle(ctx: RequestContext): Promise<void>
handle
(
const ctx: RequestContext
ctx
); // every resolution in this scope sees the same ctx
} // scope disposed here, even if handle() throws

await using disposes the scope at the end of the block. Without it, call await scope.dispose() in a finally. Read Lifetimes and Scopes for the rules.

Turn on validation in development and tests. It catches missing registrations and lifetime mistakes when the provider is built, not when a rare code path first runs.

class
class Database
Database
{
async
Database.query(sql: string): Promise<{
id: number;
}[]>
query
(
sql: string
sql
: string):
interface Promise<T>

Represents the completion of an asynchronous operation

Promise
<{
id: number
id
: number }[]> {
return [];
}
async
Database.onDestroy(): Promise<void>
onDestroy
() {
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
('pool closed'); // runs on provider.dispose()
}
}
class
class FakeDatabase
FakeDatabase
extends
class Database
Database
{
override async
FakeDatabase.query(): Promise<{
id: number;
}[]>
query
() {
return [{
id: number
id
: 1 }];
}
}
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
().
ServiceCollection.addSingleton<Database>(token: Newable<Database, []>): 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
(
class Database
Database
);
// In a test: replace one registration, keep the rest.
const services: ServiceCollection
services
.
ServiceCollection.replace<Database>(token: Token<Database>, implementation: Newable<NoInfer<Database>, []>): ServiceCollection (+2 overloads)

Replaces the plain registrations of token, keeping the lifetime of the last one (singleton if there was none).

replace
(
class Database
Database
,
class FakeDatabase
FakeDatabase
);
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,
BuildOptions.validateOnBuild?: boolean | undefined

Check every registration up front (missing dependencies, captive scoped services) and throw a ValidationError.

validateOnBuild
: true });
const
const db: Database
db
= await
const provider: ServiceProvider
provider
.
ServiceProvider.getRequiredService<Database>(token: Token<Database>): Promise<Database>

Resolves token, throwing ServiceNotFoundError if it is not registered.

getRequiredService
(
class Database
Database
);
await
const db: Database
db
.
Database.query(sql: string): Promise<{
id: number;
}[]>
query
('SELECT 1'); // [{ id: 1 }]
await
const provider: ServiceProvider
provider
.
ServiceProvider.dispose(): Promise<void>

Disposes every instance this provider (or scope) created, in reverse creation order: onDestroy(), else [Symbol.asyncDispose](), else [Symbol.dispose](). Disposing the root disposes singletons; disposing a scope disposes its scoped and transient instances. Every instance is disposed even if some throw; the errors are rethrown afterwards (an AggregateError if more than one). Later calls wait for the first one and do not rethrow.

dispose
(); // logs "pool closed"