Skip to Content
Modules

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.

Modules

As your application grows, putting every provider into a single global list becomes unmanageable. You lose the ability to encapsulate logic, and it becomes difficult to track where dependencies are coming from.

Modules solve this by grouping related providers into bounded contexts. A module defines its own internal set of providers and explicitly chooses which ones to share with the rest of the application.

Encapsulation and Visibility

The primary purpose of a module is to control visibility. defineModule({ name, providers, imports, exports }) declares one, and from this page on Nexus.create takes the program’s root module. A module sees its own providers and the exports of the modules it imports, and nothing else.

  • Private Providers: By default, everything provided inside a module is private. Only other providers within the same module can use them.
  • Public Exports: To make a provider available to other modules, you must list its token in the exports array.
import { Nexus, Token, defineModule, provide } from '@nexusdi/core'; interface IReactorCore { readonly output: number; } interface IPowerRouter { divert(): number; } interface IShipComputer { status(): string; } const REACTOR = new Token<IReactorCore>('ReactorCore'); const POWER_ROUTER = new Token<IPowerRouter>('PowerRouter'); const COMPUTER = new Token<IShipComputer>('ShipComputer'); class FusionReactor implements IReactorCore { readonly output = 1.21; } class PlasmaRouter implements IPowerRouter { static deps = [REACTOR] as const; constructor(private readonly reactor: IReactorCore) {} divert() { return this.reactor.output / 2; } } class QuantumComputer implements IShipComputer { static deps = [POWER_ROUTER] as const; constructor(private readonly router: IPowerRouter) {} status() { return `ShipComputer online on ${this.router.divert()} GW.`; } } const Engineering = defineModule({ name: 'Engineering', providers: [ provide(REACTOR, { useClass: FusionReactor }), provide(POWER_ROUTER, { useClass: PlasmaRouter }), provide(COMPUTER, { useClass: QuantumComputer }), ], exports: [COMPUTER], }); const Meridian = defineModule({ name: 'Meridian', imports: [Engineering] }); await using ship = await Nexus.create(Meridian); const status = ship.get(COMPUTER).status(); // -> 'ShipComputer online on 0.605 GW.' console.log(status); const routerVisible = ship.has(POWER_ROUTER); // -> false console.log(routerVisible); const inside = ship.get(POWER_ROUTER, { module: Engineering }).divert(); // -> 0.605 console.log(inside);

Engineering provides a power router and a computer, both built on a reactor, and exports only COMPUTER. Meridian imports Engineering, so the root container resolves COMPUTER. POWER_ROUTER stays private: has() answers false for it, and get(POWER_ROUTER, { module: Engineering }) resolves it as code inside Engineering would.

has(token) answers whether get(token) would find a visible provider, and never builds anything. The { module } option of get() and has() resolves as if the call came from inside that module, which suits tests and debugging sessions.

Try it: Save the program as modules.ts and run npx tsx modules.ts. It prints ShipComputer online on 0.605 GW., then false, then 0.605. Add POWER_ROUTER to the exports of Engineering and run it again. routerVisible prints true, and the other two lines stay the same. Remove it again and routerVisible prints false.

This pattern prevents “leaky abstractions.” For example, a DatabaseModule might provide a ConnectionPool internally, but only export a Repository to the rest of the app. The rest of the app doesn’t need to know the pool exists, and they cannot accidentally depend on it.

Reaching a Private Token

get() throws NEXUS_NOT_VISIBLE when a provider for the token exists but the caller cannot see it, and the message names the modules that own it:

import { Nexus, Token, defineModule, provide } from '@nexusdi/core'; import { isNexusError } from '@nexusdi/core'; interface IPowerRouter { divert(): number; } const POWER_ROUTER = new Token<IPowerRouter>('PowerRouter'); class PlasmaRouter implements IPowerRouter { divert() { return 0.605; } } const Engineering = defineModule({ name: 'Engineering', providers: [provide(POWER_ROUTER, { useClass: PlasmaRouter })], }); await using ship = await Nexus.create( defineModule({ name: 'Meridian', imports: [Engineering] }), ); let message: string | null = null; try { ship.get(POWER_ROUTER); } catch (error) { if (!isNexusError(error)) throw error; message = error.message; } const seen = message; // -> '[NEXUS_NOT_VISIBLE] token=PowerRouter owners=Engineering. https://nexus.js.org/errors/NEXUS_NOT_VISIBLE' console.log(seen);

The same rule holds between modules, and Nexus.create checks the dependencies of every provider before it builds anything. A dependency on a token that another module keeps private is reported as NEXUS_MISSING_PROVIDER inside NEXUS_BLUEPRINT_INVALID. NEXUS_NOT_VISIBLE comes only from a get() of a token that a module owns and does not export. Tactical below imports Engineering, which keeps REACTOR to itself, so the drone in Tactical has no visible reactor:

import { Nexus, Token, defineModule, provide } from '@nexusdi/core'; import { isNexusError } from '@nexusdi/core'; interface IReactorCore { readonly output: number; } interface ISurveyDrone { readonly range: number; } const REACTOR = new Token<IReactorCore>('ReactorCore'); const DRONE = new Token<ISurveyDrone>('SurveyDrone'); class FusionReactor implements IReactorCore { readonly output = 1.21; } class ScoutDrone implements ISurveyDrone { static deps = [REACTOR] as const; constructor(private readonly reactor: IReactorCore) {} get range() { return this.reactor.output * 10; } } const Engineering = defineModule({ name: 'Engineering', providers: [provide(REACTOR, { useClass: FusionReactor })], }); const Tactical = defineModule({ name: 'Tactical', imports: [Engineering], providers: [provide(DRONE, { useClass: ScoutDrone, lifetime: 'transient' })], exports: [DRONE], }); let outcome: string; try { await using ship = await Nexus.create( defineModule({ name: 'Meridian', imports: [Tactical] }), ); outcome = `range ${ship.get(DRONE).range}`; } catch (caught) { if (!isNexusError(caught, 'NEXUS_BLUEPRINT_INVALID')) throw caught; outcome = caught.errors[0]?.message ?? 'no error'; } const result = outcome; // -> '[NEXUS_MISSING_PROVIDER] token=ReactorCore requester=SurveyDrone module=Tactical. https://nexus.js.org/errors/NEXUS_MISSING_PROVIDER' console.log(result);

Try it: Run the second program. It prints the NEXUS_MISSING_PROVIDER message above. Add exports: [REACTOR] to Engineering and run it again. create resolves and the program prints range 12.1, the range the drone takes from the reactor. Remove the export and the message returns.

Module Integration

Modules interact through imports. If Module A imports Module B, Module A can see everything that Module B exports.

Global Modules

Some providers are truly universal (like configuration or logging) and are needed by almost every module. You can mark a module as global: true.

import { Nexus, Token, defineModule, provide } from '@nexusdi/core'; interface IReactorCore { readonly output: number; } interface ISurveyDrone { readonly range: number; } const REACTOR = new Token<IReactorCore>('ReactorCore'); const DRONE = new Token<ISurveyDrone>('SurveyDrone'); class FusionReactor implements IReactorCore { readonly output = 1.21; } class ScoutDrone implements ISurveyDrone { static deps = [REACTOR] as const; constructor(private readonly reactor: IReactorCore) {} get range() { return this.reactor.output * 10; } } const Engineering = defineModule({ name: 'Engineering', global: true, providers: [provide(REACTOR, { useClass: FusionReactor })], exports: [REACTOR], }); const Tactical = defineModule({ name: 'Tactical', providers: [provide(DRONE, { useClass: ScoutDrone, lifetime: 'transient' })], exports: [DRONE], }); await using ship = await Nexus.create( defineModule({ name: 'Meridian', imports: [Engineering, Tactical] }), ); const range = ship.get(DRONE).range; // -> 12.1 console.log(range);

A global module’s exports are visible to every module in the graph automatically, provided the global module is included in the graph at least once. Tactical above names no import, and its drone still reaches the reactor Engineering exports.

A global module suits a provider that most modules need and no module owns, such as configuration. Every other provider belongs behind an explicit import, because the import is what shows a reader where a dependency comes from.

Try it: Run the program and it prints 12.1. Remove global: true from Engineering and run it again. create rejects with NEXUS_BLUEPRINT_INVALID, and its errors list holds one NEXUS_MISSING_PROVIDER for ReactorCore, requested by SurveyDrone in Tactical. Put global: true back and it prints 12.1 again.

Swapping Implementations

Because modules are defined by the tokens they export and not by the classes they use, you can swap an entire module for another as long as the exported tokens remain the same.

This is incredibly powerful for testing. You can replace a ProductionEngineeringModule with a SimulatorEngineeringModule that exports the same COMPUTER token but uses a simulated reactor behind that token. In this page’s example, Engineering is the module you replace, and SimulatorEngineering binds REACTOR to a SimulatedReactor.

import { Nexus, Token, defineModule, provide } from '@nexusdi/core'; import type { ModuleRef } from '@nexusdi/core'; interface IReactorCore { readonly output: number; } interface IShipComputer { status(): string; } const REACTOR = new Token<IReactorCore>('ReactorCore'); const COMPUTER = new Token<IShipComputer>('ShipComputer'); class FusionReactor implements IReactorCore { readonly output = 1.21; } class SimulatedReactor implements IReactorCore { readonly output = 0; } class QuantumComputer implements IShipComputer { static deps = [REACTOR] as const; constructor(private readonly reactor: IReactorCore) {} status() { return `Reactor output ${this.reactor.output} GW.`; } } const Engineering = defineModule({ name: 'Engineering', providers: [ provide(REACTOR, { useClass: FusionReactor }), provide(COMPUTER, { useClass: QuantumComputer }), ], exports: [COMPUTER], }); const SimulatorEngineering = defineModule({ name: 'SimulatorEngineering', providers: [ provide(REACTOR, { useClass: SimulatedReactor }), provide(COMPUTER, { useClass: QuantumComputer }), ], exports: [COMPUTER], }); async function statusWith(engineering: ModuleRef) { await using ship = await Nexus.create( defineModule({ name: 'Meridian', imports: [engineering] }), ); return ship.get(COMPUTER).status(); } const flight = await statusWith(Engineering); // -> 'Reactor output 1.21 GW.' console.log(flight); const simulator = await statusWith(SimulatorEngineering); // -> 'Reactor output 0 GW.' console.log(simulator);

The consuming code doesn’t change; it just receives a different implementation of the same token.

A swap works only when the replacement exports every token the original did. A missing export passes create when no provider depends on the token, and fails only at get() with NEXUS_NOT_VISIBLE.

Try it: Run the program. It prints Reactor output 1.21 GW. and then Reactor output 0 GW.. Remove COMPUTER from the exports of SimulatorEngineering and run it again. create still resolves, because no provider depends on COMPUTER and the dependency check has nothing to report. Only get(COMPUTER) for the simulator ship throws NEXUS_NOT_VISIBLE, because Meridian can no longer see its computer. The first line still prints. Restore the export and both lines print again.