Skip to Content
NexusDI

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.

NexusDI

NexusDI is a dependency injection container for TypeScript that checks the whole module graph when the container starts. After startup every get() is synchronous, and every wiring mistake has already been reported with its code.

The eight-provider benchmark app built with NexusDI is 19.0 kB minified and gzipped by esbuild. NexusDI compared sets that figure beside the same app built with four other containers.

What NexusDI does when the container starts

Nexus.create compiles every module, provider and dependency before it builds anything, then builds every singleton and awaits every async factory. A missing provider, a dependency cycle, a lifetime mistake or an invalid provider fails create in one error that lists every problem it found. When create resolves, the container is sealed, and get() returns an instance with no await. @nexusdi/core has no runtime dependencies.

This program builds the Starship Meridian: five modules, an option passed through forRoot(), an async factory and a transient drone. It runs as written with npx tsx meridian.ts.

import { Nexus, Token, defineModule, provide } from '@nexusdi/core'; interface IReactorCore { readonly output: number; } interface IShipComputer { status(): string; } interface ISubspaceLink { readonly frequency: number; send(message: string): string; } interface INavCharts { plot(target: string): string; } interface ISurveyDrone { survey(target: string): string; } interface IFlightLog { readonly entries: string[]; } interface CommsOptions { readonly frequency: number; readonly transport: 'relay' | 'laser'; } const REACTOR = new Token<IReactorCore>('ReactorCore'); const COMPUTER = new Token<IShipComputer>('ShipComputer'); const COMMS_OPTIONS = new Token<CommsOptions>('CommsOptions'); const SUBSPACE_LINK = new Token<ISubspaceLink>('SubspaceLink'); const NAV_CHARTS = new Token<INavCharts>('NavCharts'); const DRONE = new Token<ISurveyDrone>('SurveyDrone'); const FLIGHT_LOG = new Token<IFlightLog>('FlightLog'); class FusionReactor implements IReactorCore { readonly output = 1.21; } class QuantumComputer implements IShipComputer { static deps = [REACTOR] as const; constructor(private readonly reactor: IReactorCore) {} status() { return `ShipComputer online. Reactor output ${this.reactor.output} GW.`; } } class SubspaceRelay implements ISubspaceLink { constructor(readonly frequency: number) {} send(message: string) { return `relay ${this.frequency}: ${message}`; } } class LaserLink implements ISubspaceLink { constructor(readonly frequency: number) {} send(message: string) { return `laser ${this.frequency}: ${message}`; } } class ScoutDrone implements ISurveyDrone { static deps = [COMPUTER] as const; constructor(private readonly computer: IShipComputer) {} survey(target: string) { return `${target} surveyed. ${this.computer.status()}`; } } class ShuttleFlightLog implements IFlightLog { readonly entries: string[] = []; } const Engineering = defineModule({ name: 'Engineering', providers: [ provide(REACTOR, { useClass: FusionReactor }), provide(COMPUTER, { useClass: QuantumComputer }), ], exports: [COMPUTER], }); const Comms = defineModule({ name: 'Comms', options: COMMS_OPTIONS, providers: [ provide(SUBSPACE_LINK, { useFactory: (options) => options.transport === 'laser' ? new LaserLink(options.frequency) : new SubspaceRelay(options.frequency), deps: [COMMS_OPTIONS], }), ], exports: [SUBSPACE_LINK], }); const Tactical = defineModule({ name: 'Tactical', imports: [ Engineering, Comms.forRoot({ frequency: 1420, transport: 'laser' }), ], providers: [ provide(NAV_CHARTS, { useFactory: async (link) => ({ plot: (target: string) => link.send(`course to ${target}`), }), deps: [SUBSPACE_LINK], }), provide(DRONE, { useClass: ScoutDrone, lifetime: 'transient' }), ], exports: [NAV_CHARTS, DRONE], }); const BridgeApi = defineModule({ name: 'BridgeApi', providers: [ provide(FLIGHT_LOG, { useClass: ShuttleFlightLog, lifetime: 'scoped' }), ], exports: [FLIGHT_LOG], }); const Meridian = defineModule({ name: 'Meridian', imports: [Tactical, BridgeApi], }); await using ship = await Nexus.create(Meridian); const course = ship.get(NAV_CHARTS).plot('Kepler-442b'); // -> 'laser 1420: course to Kepler-442b' console.log(course);

NexusDI needs no experimentalDecorators or emitDecoratorMetadata setting. A class lists its dependencies in static deps, and TypeScript checks that list against the constructor. Decorators are optional and come from @nexusdi/decorators. NexusDI compared shows the result under each toolchain the benchmarks run.

Try it: save the program as meridian.ts in the project from Getting started and run it. The terminal prints the plotted course. The Modules page shows what Nexus.create reports when a module keeps a token private.

When to reach for NexusDI

NexusDI suits an application with enough services that wiring them by hand hides mistakes until a code path runs. These four cases gain the most:

  1. A codebase where several entry points share one graph, such as an API and a background worker.
  2. A server that needs values per request, such as the current user or a transaction, through scopes.
  3. Resources that must shut down in dependency order, through await using and Symbol.asyncDispose.
  4. Tests that replace one service and keep the rest of the real graph, through @nexusdi/testing.

When to skip NexusDI

A script with a handful of objects needs no container: a few new calls in one file are clearer. A framework with its own container, such as NestJS or Angular, already wires its services, and NexusDI would add a second container beside the first.

Where to start with NexusDI