Module ReferenceFundamentals · NestJS

Ask the container for providers by hand with ModuleRef: get() by class, resolve() for a transient instance, a global lookup with strict false, and create() for a class the container does not own.

What you will learn

Read the theory for Module Reference

All Fundamentals lessons

All NestJS courses

loading types…

What you'll learn

  • Retrieve a provider chosen at runtime with ModuleRef.get(), and know why get() refuses scoped providers
  • Resolve a transient provider per call with resolve(), and share a sub-tree between calls with a context id
  • Look a provider up across module boundaries with { strict: false }, and instantiate an unregistered class with create()

Module Reference

Constructor injection answers one question, "what does this class need", once, when the class is written. Some questions are only answered while the application runs: which of three formatters the client asked for, a fresh instance of something for this one operation, a class the container was never told about. ModuleRef is the container itself, injectable like any provider, and it lets a class ask those questions by hand. Use it when injection cannot express what you need, and not instead of injection when it can: a class that fetches everything from ModuleRef hides its dependencies, which is what injection existed to avoid.

Getting a provider

moduleRef.get(token) returns the instance registered under a token in the current module:

@Injectable()
export class PaymentsService {
  constructor(private readonly moduleRef: ModuleRef) {}

  charge(method: 'card' | 'transfer', amount: number) {
    const gateway = this.moduleRef.get(method === 'card' ? CardGateway : TransferGateway);
    return gateway.charge(amount);
  }
}

The token can be a class, a string or a symbol, exactly as in @Inject(). Two limits. get() looks in the module the calling class belongs to, so a provider that lives elsewhere and is not imported is not found; pass { strict: false } as a second argument to search the whole application instead. And get() returns only singletons: for a request-scoped or transient provider it throws, with a message naming resolve(), because there is no single instance to return.

Resolving a scoped provider

moduleRef.resolve(token) is get() for scoped providers. It is asynchronous, and every call builds a new instance from its own sub-tree of the container:

const first = await this.moduleRef.resolve(TransientService);
const second = await this.moduleRef.resolve(TransientService);
console.log(first === second); // false

When several resolutions should share one sub-tree, so that they see the same request-scoped providers, make a context id and pass it to each:

const contextId = ContextIdFactory.create();
const [a, b] = await Promise.all([
  this.moduleRef.resolve(TransientService, contextId),
  this.moduleRef.resolve(TransientService, contextId),
]);
console.log(a === b); // true

A sub-tree created this way has no request in it; registerRequestByContextId(request, contextId) supplies one when a provider in it injects REQUEST. Inside a request, ContextIdFactory.getByRequest(request) returns the id of the sub-tree that request is already using, so a resolution joins it instead of starting another.

Creating what the container does not own

moduleRef.create(SomeClass) instantiates a class that is registered nowhere, resolving its constructor's dependencies from the container as if it were a provider:

const exporter = await this.moduleRef.create(CsvExporter);

The class needs @Injectable(), not to be registered, but because that decorator is what makes TypeScript emit the constructor's parameter types; without it the container has no idea what to pass. create() is for objects with dependencies that are made on demand and thrown away: a report, an exporter, a one-off job.

Your task

The cats API exports itself in the format the client asks for, numbers every export, and builds a report on demand.

  1. In ExportService.export(), fetch the formatter for the requested format from the container, chosen by class, and take a fresh Ticket for each export. Ticket is transient; read the error you get from the wrong method.
  2. In ExportService.report(), have the container build a Report, which no module registers, and return what it builds.
  3. StatsController lives in the root module and CatsModule does not export CatsService. Count the cats anyway.

When it fails

  • Ticket is marked as a scoped provider. Request and transient-scoped providers can't be used in combination with "get()" method: Nest's own words; use resolve(), and await it.
  • Every export has the same ticket number: the ticket was resolved once and kept, or get() was replaced by a single stored instance. Resolve inside the method, per call.
  • Nest could not find CatsService element: get() searched only the current module. Pass { strict: false }.
  • Report builds with total: 0 while cats exist, or throws Cannot read properties of undefined: the class lost its @Injectable(), so create() had no parameter types and passed nothing.

Remember

  • ModuleRef is the container, injectable. get(token) for singletons in this module, { strict: false } for the whole application.
  • resolve(token) for scoped providers, a new instance per call; share a sub-tree with a context id.
  • create(Class) instantiates an unregistered class and resolves its dependencies; it needs @Injectable() for the metadata.
  • Prefer injection when the dependency is known when the class is written.
Stuck? Show a hint

export(): this.moduleRef.get(format === 'csv' ? CsvFormatter : JsonFormatter), then await this.moduleRef.resolve(Ticket). report(): await this.moduleRef.create(Report), then build(). stats(): this.moduleRef.get(CatsService, { strict: false }).