This page documents the 0.4 release candidates of @nexusdi/core. The newest on npm is 0.4.0-rc.1. The documentation for 0.3.2, the current release, is at nexus.js.org. Try the release candidate.
Configurable Modules
In a real-world application, you often need the same module to behave differently depending on the environment or the specific instance. For example, a CommsModule might connect to a local relay in development but a satellite link in production.
Configurable Modules allow you to define a module that requires a set of options to be provided during its import. This allows you to maintain a single module definition while varying its behavior through configuration.
Defining Options with defineModule
To make a module configurable, you specify an options token in the defineModule call. This token acts as the “configuration slot” for the module. The module’s providers list that token in their deps like any other token.
import { Nexus, Token, defineModule, provide } from '@nexusdi/core';
interface CommsOptions {
readonly frequency: number;
readonly transport: 'relay' | 'laser';
}
interface ISubspaceLink {
hail(): string;
}
const COMMS_OPTIONS = new Token<CommsOptions>('CommsOptions');
const SUBSPACE_LINK = new Token<ISubspaceLink>('SubspaceLink');
class SubspaceRelay implements ISubspaceLink {
static deps = [COMMS_OPTIONS] as const;
constructor(private readonly options: CommsOptions) {}
hail() {
return `relay on ${this.options.frequency}`;
}
}
const Comms = defineModule({
name: 'Comms',
options: COMMS_OPTIONS,
providers: [provide(SUBSPACE_LINK, { useClass: SubspaceRelay })],
exports: [SUBSPACE_LINK],
});
const Meridian = defineModule({
name: 'Meridian',
imports: [Comms.forRoot({ frequency: 1420, transport: 'relay' })],
});
await using ship = await Nexus.create(Meridian);
const hail = ship.get(SUBSPACE_LINK).hail(); // -> 'relay on 1420'
console.log(hail);The forRoot(value) method allows an importer to supply the required options. forRoot() returns a module, so it goes in imports where the plain module would go. SubspaceRelay declares static deps = [COMMS_OPTIONS] as const, and NexusDI hands it the value forRoot() received.
The type of that value comes from the token, so Comms.forRoot({ frequency: '1420', transport: 'relay' }) fails to compile: tsc reports Type 'string' is not assignable to type 'number' on the frequency line.
If a module declares an options token but is imported without a forRoot call, Nexus.create rejects with NEXUS_BLUEPRINT_INVALID, and its errors list holds NEXUS_MODULE_OPTIONS_MISSING. The error carries module=Comms. See NEXUS_MODULE_OPTIONS_MISSING. This region catches the error and prints the inner codes:
import { Nexus, Token, defineModule, provide } from '@nexusdi/core';
import { isNexusError } from '@nexusdi/core';
interface CommsOptions {
readonly frequency: number;
readonly transport: 'relay' | 'laser';
}
interface ISubspaceLink {
hail(): string;
}
const COMMS_OPTIONS = new Token<CommsOptions>('CommsOptions');
const SUBSPACE_LINK = new Token<ISubspaceLink>('SubspaceLink');
class SubspaceRelay implements ISubspaceLink {
static deps = [COMMS_OPTIONS] as const;
constructor(private readonly options: CommsOptions) {}
hail() {
return `relay on ${this.options.frequency}`;
}
}
const Comms = defineModule({
name: 'Comms',
options: COMMS_OPTIONS,
providers: [provide(SUBSPACE_LINK, { useClass: SubspaceRelay })],
exports: [SUBSPACE_LINK],
});
const error = await Nexus.create(
defineModule({ name: 'Meridian', imports: [Comms] }),
).catch((caught: unknown) => caught);
const inner = isNexusError(error, 'NEXUS_BLUEPRINT_INVALID')
? error.errors
: [];
const codes = inner.map((each) => each.code); // -> ['NEXUS_MODULE_OPTIONS_MISSING']
console.log(codes);Try it: Save the first region as comms.ts and run npx tsx comms.ts. It prints relay on 1420. Change the import to Comms with no forRoot() call and run it again. tsc accepts the change, and Nexus.create rejects with the error above. Put the forRoot() call back and it prints relay on 1420 again.
Dynamic Implementations
Options aren’t just for simple values; they can be used to determine which class should fulfill a token.
By using a factory for a provider and reading the options token, you can switch implementations based on the configuration. Comms binds SUBSPACE_LINK in one factory over COMMS_OPTIONS, and the factory builds a LaserLink or a SubspaceRelay from the transport option.
import { Nexus, Token, defineModule, provide } from '@nexusdi/core';
interface CommsOptions {
readonly frequency: number;
readonly transport: 'relay' | 'laser';
}
interface ISubspaceLink {
hail(): string;
}
const COMMS_OPTIONS = new Token<CommsOptions>('CommsOptions');
const SUBSPACE_LINK = new Token<ISubspaceLink>('SubspaceLink');
class SubspaceRelay implements ISubspaceLink {
constructor(private readonly frequency: number) {}
hail() {
return `relay on ${this.frequency}`;
}
}
class LaserLink implements ISubspaceLink {
constructor(private readonly frequency: number) {}
hail() {
return `laser on ${this.frequency}`;
}
}
const Comms = defineModule({
name: 'Comms',
options: COMMS_OPTIONS,
providers: [
provide(SUBSPACE_LINK, {
useFactory: (options): ISubspaceLink =>
options.transport === 'laser'
? new LaserLink(options.frequency)
: new SubspaceRelay(options.frequency),
deps: [COMMS_OPTIONS],
}),
],
exports: [SUBSPACE_LINK],
});
await using ship = await Nexus.create(
defineModule({
name: 'Meridian',
imports: [Comms.forRoot({ frequency: 1420, transport: 'laser' })],
}),
);
const hail = ship.get(SUBSPACE_LINK).hail(); // -> 'laser on 1420'
console.log(hail);This allows you to keep your components decoupled from the specific implementation, as they only depend on the interface.
Try it: Save the region as transport.ts, run it, and read laser on 1420. Change transport: 'laser' to transport: 'relay' and run it again. The output is relay on 1420, and no consumer of SUBSPACE_LINK changed.
Asynchronous Configuration with forRootAsync
Sometimes your configuration isn’t known at compile time (it might need to be fetched from a database or an environment variable). forRootAsync allows you to compute your options during the container’s startup phase. The call takes { useFactory, deps }.
import { Nexus, Token, defineModule, provide } from '@nexusdi/core';
interface CommsOptions {
readonly frequency: number;
readonly transport: 'relay' | 'laser';
}
interface ISubspaceLink {
hail(): string;
}
interface IShipComputer {
readFrequency(): Promise<number>;
}
const COMMS_OPTIONS = new Token<CommsOptions>('CommsOptions');
const SUBSPACE_LINK = new Token<ISubspaceLink>('SubspaceLink');
const COMPUTER = new Token<IShipComputer>('ShipComputer');
class QuantumComputer implements IShipComputer {
async readFrequency() {
return 1701;
}
}
class SubspaceRelay implements ISubspaceLink {
static deps = [COMMS_OPTIONS] as const;
constructor(private readonly options: CommsOptions) {}
hail() {
return `relay on ${this.options.frequency}`;
}
}
const Engineering = defineModule({
name: 'Engineering',
global: true,
providers: [provide(COMPUTER, { useClass: QuantumComputer })],
exports: [COMPUTER],
});
const Comms = defineModule({
name: 'Comms',
options: COMMS_OPTIONS,
providers: [provide(SUBSPACE_LINK, { useClass: SubspaceRelay })],
exports: [SUBSPACE_LINK],
});
await using ship = await Nexus.create(
defineModule({
name: 'Meridian',
imports: [
Engineering,
Comms.forRootAsync({
useFactory: async (computer) => ({
frequency: await computer.readFrequency(),
transport: 'relay' as const,
}),
deps: [COMPUTER],
}),
],
}),
);
const hail = ship.get(SUBSPACE_LINK).hail(); // -> 'relay on 1701'
console.log(hail);The options factory can be async. NexusDI awaits it during Nexus.create, before it builds the module’s providers. The factory’s own deps are built first, as COMPUTER is in the region above.
The factory’s deps resolve inside the module: from its own providers, from the tokens its imports export and from the global modules. Engineering is a global module here, so the COMPUTER token is visible inside Comms.
Try it: Save the region as async-comms.ts and run it. It prints relay on 1701. Remove global: true from Engineering and run it again. Nexus.create rejects with NEXUS_BLUEPRINT_INVALID, and its one inner error is NEXUS_MISSING_PROVIDER with token=ShipComputer requester=CommsOptions module=Comms: the options factory resolves its deps inside Comms, which no longer sees COMPUTER. Put global: true back and it prints relay on 1701 again.
Validating Options with Schemas
To prevent your app from starting with invalid configuration, you can attach a schema to your module. NexusDI supports any Standard Schema validator (such as Valibot). The examples use valibot 1.5.0, whose schemas implement Standard Schema, so NexusDI validates them with no valibot-specific code.
import { Nexus, Token, defineModule, provide } from '@nexusdi/core';
import { isNexusError } from '@nexusdi/core';
import * as v from 'valibot';
const CommsSchema = v.object({
frequency: v.pipe(v.number(), v.integer(), v.minValue(1)),
transport: v.picklist(['relay', 'laser']),
});
type CommsOptions = v.InferOutput<typeof CommsSchema>;
interface ISubspaceLink {
hail(): string;
}
const COMMS_OPTIONS = new Token<CommsOptions>('CommsOptions');
const SUBSPACE_LINK = new Token<ISubspaceLink>('SubspaceLink');
class SubspaceRelay implements ISubspaceLink {
static deps = [COMMS_OPTIONS] as const;
constructor(private readonly options: CommsOptions) {}
hail() {
return `relay on ${this.options.frequency}`;
}
}
const Comms = defineModule({
name: 'Comms',
options: COMMS_OPTIONS,
schema: CommsSchema,
providers: [provide(SUBSPACE_LINK, { useClass: SubspaceRelay })],
exports: [SUBSPACE_LINK],
});
const error = await Nexus.create(
defineModule({
name: 'Meridian',
imports: [Comms.forRoot({ frequency: -5, transport: 'relay' })],
}),
).catch((caught: unknown) => caught);
const code = isNexusError(error) ? error.code : null; // -> 'NEXUS_PROVIDER_FAILED'
console.log(code);
const cause = isNexusError(error) ? error.cause : undefined;
const reason = isNexusError(cause) ? cause.code : null; // -> 'NEXUS_INVALID_MODULE_OPTIONS'
console.log(reason);
const issues = isNexusError(cause, 'NEXUS_INVALID_MODULE_OPTIONS')
? cause.issues
: [];
const messages = issues.map((issue) => issue.message); // -> ['Invalid value: Expected >=1 but received -5']
console.log(messages);If the options provided to forRoot fail the schema check, Nexus.create rejects with NEXUS_PROVIDER_FAILED, whose cause is NEXUS_INVALID_MODULE_OPTIONS. The module’s providers are never built with the bad value. The cause carries the schema’s issues. See NEXUS_PROVIDER_FAILED and NEXUS_INVALID_MODULE_OPTIONS.
Try it: Install valibot with npm install valibot@1.5.0, save the region as schema.ts and run it. It prints the codes and Invalid value: Expected >=1 but received -5. Change -5 to 1420 and run it again. Nexus.create resolves, so the first two lines print null and the third prints [].
Sharing Configured Modules
Every call to forRoot() or forRootAsync() creates a new instance of the module. Each instance has its own providers. If you want multiple parts of your app to share the same configured module (and thus share its singletons), you should call forRoot once and export the resulting module constant. Import that constant everywhere the configured module is needed.
import { Nexus, Token, defineModule, provide } from '@nexusdi/core';
interface CommsOptions {
readonly frequency: number;
readonly transport: 'relay' | 'laser';
}
interface ISubspaceLink {
hail(): string;
}
interface INavCharts {
readonly link: ISubspaceLink;
}
interface IDiagnosticsPanel {
readonly link: ISubspaceLink;
}
const COMMS_OPTIONS = new Token<CommsOptions>('CommsOptions');
const SUBSPACE_LINK = new Token<ISubspaceLink>('SubspaceLink');
const NAV_CHARTS = new Token<INavCharts>('NavCharts');
const DIAGNOSTICS_PANEL = new Token<IDiagnosticsPanel>('DiagnosticsPanel');
class SubspaceRelay implements ISubspaceLink {
static deps = [COMMS_OPTIONS] as const;
constructor(private readonly options: CommsOptions) {}
hail() {
return `relay on ${this.options.frequency}`;
}
}
class StarCharts implements INavCharts {
static deps = [SUBSPACE_LINK] as const;
constructor(readonly link: ISubspaceLink) {}
}
class StatusBoard implements IDiagnosticsPanel {
static deps = [SUBSPACE_LINK] as const;
constructor(readonly link: ISubspaceLink) {}
}
const Comms = defineModule({
name: 'Comms',
options: COMMS_OPTIONS,
providers: [provide(SUBSPACE_LINK, { useClass: SubspaceRelay })],
exports: [SUBSPACE_LINK],
});
// One forRoot() call, imported by both modules: one module instance, one link.
const ShipComms = Comms.forRoot({ frequency: 1420, transport: 'relay' });
const Tactical = defineModule({
name: 'Tactical',
imports: [ShipComms],
providers: [provide(NAV_CHARTS, { useClass: StarCharts })],
exports: [NAV_CHARTS],
});
const Bridge = defineModule({
name: 'Bridge',
imports: [ShipComms],
providers: [provide(DIAGNOSTICS_PANEL, { useClass: StatusBoard })],
exports: [DIAGNOSTICS_PANEL],
});
await using ship = await Nexus.create(
defineModule({ name: 'Meridian', imports: [Tactical, Bridge] }),
);
const charts = ship.get(NAV_CHARTS);
const panel = ship.get(DIAGNOSTICS_PANEL);
const shared = charts.link === panel.link; // -> true
console.log(shared);Tactical and Bridge both import ShipComms, and both receive the same SubspaceRelay. Two separate forRoot() calls build two module instances, and each builds its own relay:
import { Nexus, Token, defineModule, provide } from '@nexusdi/core';
interface CommsOptions {
readonly frequency: number;
readonly transport: 'relay' | 'laser';
}
interface ISubspaceLink {
hail(): string;
}
interface INavCharts {
readonly link: ISubspaceLink;
}
interface IDiagnosticsPanel {
readonly link: ISubspaceLink;
}
const COMMS_OPTIONS = new Token<CommsOptions>('CommsOptions');
const SUBSPACE_LINK = new Token<ISubspaceLink>('SubspaceLink');
const NAV_CHARTS = new Token<INavCharts>('NavCharts');
const DIAGNOSTICS_PANEL = new Token<IDiagnosticsPanel>('DiagnosticsPanel');
class SubspaceRelay implements ISubspaceLink {
static deps = [COMMS_OPTIONS] as const;
constructor(private readonly options: CommsOptions) {}
hail() {
return `relay on ${this.options.frequency}`;
}
}
class StarCharts implements INavCharts {
static deps = [SUBSPACE_LINK] as const;
constructor(readonly link: ISubspaceLink) {}
}
class StatusBoard implements IDiagnosticsPanel {
static deps = [SUBSPACE_LINK] as const;
constructor(readonly link: ISubspaceLink) {}
}
const Comms = defineModule({
name: 'Comms',
options: COMMS_OPTIONS,
providers: [provide(SUBSPACE_LINK, { useClass: SubspaceRelay })],
exports: [SUBSPACE_LINK],
});
// Two forRoot() calls: two module instances, two links.
const Tactical = defineModule({
name: 'Tactical',
imports: [Comms.forRoot({ frequency: 1420, transport: 'relay' })],
providers: [provide(NAV_CHARTS, { useClass: StarCharts })],
exports: [NAV_CHARTS],
});
const Bridge = defineModule({
name: 'Bridge',
imports: [Comms.forRoot({ frequency: 1420, transport: 'relay' })],
providers: [provide(DIAGNOSTICS_PANEL, { useClass: StatusBoard })],
exports: [DIAGNOSTICS_PANEL],
});
await using ship = await Nexus.create(
defineModule({ name: 'Meridian', imports: [Tactical, Bridge] }),
);
const charts = ship.get(NAV_CHARTS);
const panel = ship.get(DIAGNOSTICS_PANEL);
const shared = charts.link === panel.link; // -> false
console.log(shared);Try it: Save the first region as shared.ts and run it. It prints true. Replace ShipComms in Bridge’s imports with a second Comms.forRoot({ frequency: 1420, transport: 'relay' }) call and run it again. It prints false, because the ship now holds two links. Put ShipComms back and it prints true again.