Skip to Content
How do I write providers with decorators?

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.

Writing Providers with Decorators

While static deps and defineModule() are the foundation of NexusDI, you can use decorators to make your dependency declarations more readable and concise.

NexusDI provides a set of standard (TC39) decorators that act as syntactic sugar. They write the same static deps and defineModule definitions that you would write by hand. This means a decorated class and an undecorated class wire exactly the same way. The decorators live in their own package:

npm install @nexusdi/decorators@next

Tooling and Compatibility

Because these are standard decorators, they require a toolchain that can compile them: tsc, TypeScript 7, esbuild, SWC, Babel, Bun, Deno, or Vite with its Babel plugin. Plain Vite 8 and Node’s type stripping cannot compile them. tsx compiles them through esbuild. The tsconfig.json from Getting Started already compiles these decorators, and npx tsx and npx tsc need no other setting.

Important: You must leave experimentalDecorators and emitDecoratorMetadata disabled in your tsconfig.json. NexusDI does not rely on compiler-emitted metadata; every dependency is explicitly named via a token in the decorator. A project that must keep experimentalDecorators for another library uses static deps and defineModule(), as Using NexusDI with experimentalDecorators shows.

Declaring Dependencies with @Injectable

The @Injectable decorator records a class’s dependencies directly on the class definition. This replaces the need for a static deps array.

import { Nexus, Token, provide } from '@nexusdi/core'; import { Injectable } from '@nexusdi/decorators'; 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; } @Injectable({ deps: [REACTOR] }) class QuantumComputer implements IShipComputer { constructor(private readonly reactor: IReactorCore) {} status() { return `ShipComputer online. Reactor output ${this.reactor.output} GW.`; } } await using ship = await Nexus.create([ provide(REACTOR, { useClass: FusionReactor }), provide(COMPUTER, { useClass: QuantumComputer }), ]); const status = ship.get(COMPUTER).status(); // -> 'ShipComputer online. Reactor output 1.21 GW.' console.log(status);

TypeScript verifies the tokens you provide against the constructor parameters, ensuring type safety. A useClass binding reads what @Injectable declared, so provide(COMPUTER, { useClass: QuantumComputer }) needs no deps option. @Injectable also takes lifetime.

If you omit the decorator and the class has constructor parameters, Nexus.create will reject with a NEXUS_MISSING_DEPS error during startup. The rejection is NEXUS_BLUEPRINT_INVALID, whose errors list holds NEXUS_MISSING_DEPS. This region catches the rejection and prints that code:

import { Nexus, Token, isNexusError, provide } 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; } // The @Injectable line is gone, so nothing declares the constructor's deps. class QuantumComputer implements IShipComputer { constructor(private readonly reactor: IReactorCore) {} status() { return `ShipComputer online. Reactor output ${this.reactor.output} GW.`; } } const error = await Nexus.create([ provide(REACTOR, { useClass: FusionReactor }), provide(COMPUTER, { useClass: QuantumComputer }), ]).catch((caught: unknown) => caught); const errors = isNexusError(error, 'NEXUS_BLUEPRINT_INVALID') ? error.errors : []; const codes = errors.map((inner) => inner.code); // -> ['NEXUS_MISSING_DEPS'] console.log(codes);

Try it: Save the first region as decorators.ts and run npx tsx decorators.ts. The terminal prints ShipComputer online. Reactor output 1.21 GW. Change deps: [REACTOR] to deps: [COMPUTER]: npx tsc reports TS2322 on that line, because COMPUTER is not a Token<IReactorCore>. Change it back. Then delete the @Injectable({ deps: [REACTOR] }) line and run npx tsx decorators.ts again: Nexus.create rejects with the error above.

Property Injection with @Inject

Sometimes you need to inject a dependency into a class property and not the constructor. You can achieve this using the @Inject decorator on a class accessor.

import { Nexus, Token, optional, provide } from '@nexusdi/core'; import { Inject } from '@nexusdi/decorators'; interface INavCharts { plot(to: string): string; } interface ISubspaceLink { readonly frequency: number; } interface IBridge { course(to: string): string; hailing(): string; } const NAV_CHARTS = new Token<INavCharts>('NavCharts'); const SUBSPACE_LINK = new Token<ISubspaceLink>('SubspaceLink'); const BRIDGE = new Token<IBridge>('Bridge'); class Bridge implements IBridge { @Inject(NAV_CHARTS) accessor charts!: INavCharts; @Inject(optional(SUBSPACE_LINK)) accessor link!: ISubspaceLink | undefined; course(to: string) { return this.charts.plot(to); } hailing() { return this.link === undefined ? 'no link' : `${this.link.frequency} MHz`; } } await using ship = await Nexus.create([ provide(NAV_CHARTS, { useValue: { plot: (to: string) => `course to ${to}` }, }), provide(BRIDGE, { useClass: Bridge }), ]); const course = ship.get(BRIDGE).course('Kepler-442b'); // -> 'course to Kepler-442b' console.log(course); const hail = ship.get(BRIDGE).hailing(); // -> 'no link' console.log(hail);

The accessor keyword is required because standard decorators operate on the accessor, and the ! (definite assignment) tells TypeScript that NexusDI will fill the property during instantiation. @Inject takes the dependency modifiers as well: @Inject(optional(SUBSPACE_LINK)) leaves the property undefined when nothing provides the token, and @Inject(lazy(TOKEN)) fills it with a thunk.

Try it: Save the region as accessor.ts and run npx tsx accessor.ts. It prints course to Kepler-442b and no link. Add provide(SUBSPACE_LINK, { useValue: { frequency: 1420 } }) to the array passed to Nexus.create and run it again: the second line is 1420 MHz. Delete the line and it is no link again.

Declarative Modules with @Module

You can turn a class into a NexusDI module using the @Module decorator. This names the module after the class and encapsulates its providers and exports.

import { Nexus, Token, provide } from '@nexusdi/core'; import { Injectable, Module } from '@nexusdi/decorators'; 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; } @Injectable({ deps: [REACTOR] }) class QuantumComputer implements IShipComputer { constructor(private readonly reactor: IReactorCore) {} status() { return `ShipComputer online. Reactor output ${this.reactor.output} GW.`; } } @Module({ providers: [ provide(REACTOR, { useClass: FusionReactor }), provide(COMPUTER, { useClass: QuantumComputer }), ], exports: [COMPUTER], }) class Engineering {} @Module({ imports: [Engineering] }) class Command {} await using ship = await Nexus.create(Command); const status = ship.get(COMPUTER).status(); // -> 'ShipComputer online. Reactor output 1.21 GW.' console.log(status); const hidden = ship.has(REACTOR); // -> false console.log(hidden);

Module encapsulation rules apply exactly as they do with defineModule(): only tokens listed in exports are visible to modules that import this class. Nexus.create(Command) takes the decorated class where it takes a defineModule() result. Engineering exports COMPUTER and keeps REACTOR private, so ship.has(REACTOR) is false.

Try it: Save the region as module.ts and run npx tsx module.ts: it prints the status line and false. Add REACTOR to exports and run it again, and the last line is true.

Note: @Module classes are static. If you need a configurable module (e.g., one with forRoot() or forRootAsync()), you should continue using defineModule(). See Configurable Modules. A @Module class may import a configurable module.