NestJS — Enterprise Node.js

ModuleRef, Lazy Loading & Dynamic Resolution

18 min Lesson 51 of 80

ModuleRef, Lazy Loading & Dynamic Resolution

Most dependencies should be injected through constructors, but advanced applications sometimes need dynamic provider lookup: plugins, optional integrations, tenant-specific services, or a module that should load only when a feature is requested. ModuleRef and LazyModuleLoader cover those cases.

Core idea

This feature is about controlling how the application is organized and how it behaves at runtime. These are the points a developer should understand before using it in a real project:

  • ModuleRef.get() retrieves an already-created provider by token inside the current module context.
  • Passing { strict: false } lets you resolve a provider from the global application context, but overuse can hide module-boundary problems.
  • moduleRef.resolve() creates or retrieves scoped providers using a ContextId, which matters for request-scoped dependencies.
  • LazyModuleLoader loads an entire module on demand instead of during application bootstrap.
  • Dynamic resolution is powerful but should be reserved for framework, plugin, and integration code rather than ordinary business services.

Practical example

The following example shows the idea in a practical NestJS project. The goal is not to memorize the snippet, but to understand where it belongs in the architecture:

@Injectable() export class ReportPluginRunner { constructor( private readonly moduleRef: ModuleRef, private readonly lazyLoader: LazyModuleLoader, ) {} async runHeavyExport() { const moduleRef = await this.lazyLoader.load(() => ExportModule); const exporter = moduleRef.get(CsvExportService); return exporter.generate(); } }
Design note: Constructor injection remains the default because it is explicit and testable. Reach for ModuleRef when the dependency cannot be known at compile time or should not be created during bootstrap.

Production checklist

  • Prefer constructor injection for normal application services.
  • Use LazyModuleLoader for rarely used heavy modules.
  • Avoid { strict: false } unless you intentionally need cross-module lookup.
  • Use ContextIdFactory when resolving request-scoped providers manually.
Rule of thumb: If the feature makes boundaries clearer and tests easier, it is probably the right choice. If it hides dependencies or makes tracing harder, redesign.

Summary

This lesson covers an advanced NestJS area that matters when building enterprise applications. Focus on clear boundaries, testable behavior, and choosing the right tool for the context instead of using every feature everywhere.