使用模块引用 API 提供扩展点
通过 ModuleRef 以命令式方式解析有作用域的依赖,为第三方扩展提供支持。
使用模块引用 API 提供扩展点 是 CoddyKit 上的免费 NestJS Enterprise Backend APIs 课时。 这是第 4 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 NestJS Enterprise Backend APIs 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 NestJS Enterprise Backend APIs 课程共包含 4 节课。
本课时的部分内容尚未翻译,以英文显示。
Why ModuleRef Exists
NestJS resolves dependencies declaratively: you list providers in a constructor and the container wires them. But a plugin host can't know its extensions at compile time. You need to ask the container for a provider imperatively, at runtime.
ModuleRef is the DI container's public handle. It lets you:
- Retrieve an existing singleton by token (
get). - Resolve a scoped (REQUEST / TRANSIENT) instance on demand (
resolve). - Instantiate a class that was never registered as a provider (
create).
This is the foundation for plugin architectures and hexagonal extension points where adapters are chosen dynamically.
Injecting ModuleRef
ModuleRef is itself an injectable. Add it to any provider's constructor and Nest hands you the reference to the module instance that owns this provider.
Note that you cannot call get() in the constructor body if the dependency isn't ready yet — prefer onModuleInit for eager lookups so the whole module tree is constructed first.
import { Injectable, OnModuleInit } from '@nestjs/common';
import { ModuleRef } from '@nestjs/core';
import { AuditService } from './audit.service';
@Injectable()
export class PluginHost implements OnModuleInit {
private audit: AuditService;
constructor(private readonly moduleRef: ModuleRef) {}
onModuleInit() {
// Safe here: all providers are already instantiated.
this.audit = this.moduleRef.get(AuditService);
}
}get() — Retrieving Singletons
moduleRef.get(token) returns an already-instantiated singleton-scoped provider. It is synchronous and returns the same instance every call.
- By default the lookup is scoped to the current module.
- Pass
{ strict: false }to search the entire application (useful when the provider lives in another module that wasn't imported here).
get() throws if the token is request- or transient-scoped — those require resolve().
// Same module — strict (default)
const local = this.moduleRef.get(LocalCache);
// Anywhere in the app graph — non-strict
const global = this.moduleRef.get(ConfigService, { strict: false });
// String / Symbol tokens work too
const driver = this.moduleRef.get<StorageDriver>('STORAGE_DRIVER', {
strict: false,
});resolve() — Scoped Instances
Request- and transient-scoped providers do not have a single instance, so get() can't return one. Use the asynchronous resolve() instead.
Each call to resolve() returns a brand-new transient sub-tree by default. Two calls give two different instances — important when a plugin must not share mutable state.
import { Injectable } from '@nestjs/common';
import { ModuleRef } from '@nestjs/core';
import { TenantProcessor } from './tenant.processor'; // @Injectable({ scope: Scope.TRANSIENT })
@Injectable()
export class JobRunner {
constructor(private readonly moduleRef: ModuleRef) {}
async run() {
const a = await this.moduleRef.resolve(TenantProcessor);
const b = await this.moduleRef.resolve(TenantProcessor);
console.log(a === b); // false — distinct transient instances
}
}Sharing a Scoped Sub-Tree with contextId
Sometimes you want several resolve() calls to share the same request-scoped instances — for example all plugins handling one request should see the same RequestContext.
Pass a contextId to bind resolutions together. Generate one with ContextIdFactory.create() and reuse it.
import { ContextIdFactory, ModuleRef } from '@nestjs/core';
const contextId = ContextIdFactory.create();
const ctx = await this.moduleRef.resolve(RequestContext, contextId);
const svc = await this.moduleRef.resolve(ReportService, contextId);
// ctx and svc share the SAME request-scoped sub-tree
const other = await this.moduleRef.resolve(ReportService, contextId);
console.log(svc === other); // true — same contextId reuses the instanceRegistering the Request Payload
When you create your own contextId outside the normal HTTP pipeline, request-scoped providers that inject REQUEST have nothing to receive. You must register the payload manually with registerRequestByContextId.
This is the key trick for running request-scoped plugins inside cron jobs, queues, or WebSocket handlers where no Express request exists.
import { ContextIdFactory, ModuleRef, REQUEST } from '@nestjs/core';
const contextId = ContextIdFactory.create();
// Inject a synthetic 'request' for this context
this.moduleRef.registerRequestByContextId({ tenantId: 'acme' }, contextId);
// Now request-scoped providers resolve correctly off the queue
const handler = await this.moduleRef.resolve(TenantHandler, contextId);
await handler.process();create() — Instantiating Unregistered Classes
Plugins often ship classes the host never declared as providers. moduleRef.create(SomeClass) instantiates such a class while still injecting its constructor dependencies from the container.
- The result is not cached — every call builds a fresh instance.
- The class itself does not need an
@Injectable()registration, but its dependencies must be resolvable in the module graph.
This is how you load a plugin class by name and still give it access to core services.
import { Injectable } from '@nestjs/common';
import { ModuleRef } from '@nestjs/core';
// Shipped by a third party, never in any providers array
class SlackNotifier {
constructor(private readonly http: HttpClient) {}
notify(msg: string) { return this.http.post('/slack', { msg }); }
}
@Injectable()
export class ExtensionLoader {
constructor(private readonly moduleRef: ModuleRef) {}
async load() {
// http is injected from the container, SlackNotifier is not registered
const plugin = await this.moduleRef.create(SlackNotifier);
return plugin;
}
}A Token-Driven Plugin Registry
Combine a multi-provider token with ModuleRef to build an extension point. Plugins self-register under one injection token; the host resolves them and dispatches by capability.
The EXTENSIONS array is collected by Nest at boot; ModuleRef is reserved for lazy or scoped lookups the array can't express.
import { Inject, Injectable } from '@nestjs/common';
export const EXTENSIONS = Symbol('EXTENSIONS');
export interface Extension {
readonly name: string;
handle(event: unknown): Promise<void>;
}
@Injectable()
export class ExtensionDispatcher {
constructor(@Inject(EXTENSIONS) private readonly exts: Extension[]) {}
async dispatch(name: string, event: unknown) {
const ext = this.exts.find((e) => e.name === name);
if (!ext) throw new Error(`No extension: ${name}`);
await ext.handle(event);
}
}Hexagonal Adapters Chosen at Runtime
In hexagonal design the core depends on a port (interface) and stays ignorant of adapters. ModuleRef lets you pick the concrete adapter at runtime from configuration — the classic strategy-by-token pattern.
Use non-strict get() with a string token so the adapter can live in any imported module.
import { Injectable } from '@nestjs/common';
import { ModuleRef } from '@nestjs/core';
export interface PaymentPort {
charge(cents: number): Promise<string>;
}
@Injectable()
export class PaymentFacade {
constructor(private readonly moduleRef: ModuleRef) {}
private adapterToken(provider: string) {
return `PAYMENT_ADAPTER_${provider.toUpperCase()}`;
}
pick(provider: string): PaymentPort {
// e.g. PAYMENT_ADAPTER_STRIPE registered in StripeModule
return this.moduleRef.get<PaymentPort>(this.adapterToken(provider), {
strict: false,
});
}
}Modeling the Lifecycle in Plain TS
Strip away the framework and the contract is just: register adapters by key, then resolve one on demand. This standalone TypeScript program mirrors what ModuleRef.get does over a token registry — useful for unit-testing the dispatch logic in isolation.
interface PaymentPort {
charge(cents: number): string;
}
class StripeAdapter implements PaymentPort {
charge(cents: number): string {
return `stripe:charged ${cents}`;
}
}
class PaypalAdapter implements PaymentPort {
charge(cents: number): string {
return `paypal:charged ${cents}`;
}
}
class Registry {
private map = new Map<string, PaymentPort>();
register(key: string, port: PaymentPort): void {
this.map.set(key, port);
}
get(key: string): PaymentPort {
const p = this.map.get(key);
if (!p) throw new Error(`No adapter: ${key}`);
return p;
}
}
const registry = new Registry();
registry.register('stripe', new StripeAdapter());
registry.register('paypal', new PaypalAdapter());
const chosen = 'paypal';
console.log(registry.get(chosen).charge(1999));
console.log(registry.get('stripe').charge(500));Pitfalls & Best Practices
Use ModuleRef deliberately — it is an escape hatch, not a default.
- Don't call
get()in a constructor for providers that may not exist yet; useonModuleInit. - Never
get()a scoped provider — it throws; useresolve(). - Remember
resolve()andcreate()are async and uncached; awaiting in a hot path costs allocations. - Prefer constructor injection when the dependency is known statically — ModuleRef hides the graph from static analysis and tests.
- Tie request-scoped resolutions to one
contextIdand register the request payload off-pipeline.
Quick Check
You need to obtain an instance of a REQUEST-scoped provider from a queue consumer where no HTTP request exists, and all plugins for that job must share the same request-scoped state. Which approach is correct?
Recap
You learned to resolve dependencies imperatively for plugin and hexagonal extension points:
get(token, { strict })— synchronous singleton lookup, optionally across the whole app.resolve(token, contextId?)— async scoped resolution; a sharedcontextIdties instances into one sub-tree.registerRequestByContextId— injects a synthetic request so request-scoped providers work off-pipeline.create(Class)— instantiates an unregistered plugin class while still injecting its dependencies.
Reach for ModuleRef only when the dependency is dynamic; prefer plain constructor injection everywhere else.
常见问题解答
「使用模块引用 API 提供扩展点」课时是免费的吗?
是的 — 「使用模块引用 API 提供扩展点」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 NestJS Enterprise Backend APIs 课程的其余内容,请升级到 CoddyKit PRO。 NestJS Enterprise Backend APIs 课程共包含 4 节课。
「使用模块引用 API 提供扩展点」这节课中我会学到什么?
通过 ModuleRef 以命令式方式解析有作用域的依赖,为第三方扩展提供支持。 你通过在浏览器中直接运行的动手代码来练习 NestJS Enterprise Backend APIs,全天候 AI 导师会在你学习这节课的过程中回答你的问题。
学习 NestJS Enterprise Backend APIs 需要有经验吗?
无需任何先前经验。CoddyKit 上的 NestJS Enterprise Backend APIs 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 4 节课,共 4 节。
「使用模块引用 API 提供扩展点」课时需要多长时间?
大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。
我能在这节 NestJS Enterprise Backend APIs 课中编写并运行代码吗?
能。每节 NestJS Enterprise Backend APIs 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。
此课程中的所有课时
- 用于领域隔离的端口与适配器
- 使用 DiscoveryService 动态注册提供者
- 延迟加载模块与功能开关
- 使用模块引用 API 提供扩展点