[NestJS Deep Dive] 02. @Module and DynamicModule

Donghyung Ko
5 min readNov 18, 2022

Hello.
In previous posts, I covered how NestFactory creates NestApplication which is in charge of running our API server. In this post, I’d like to talk bout how the Module, one of the key components of NestJS, is registered in our application

NestJS Deep Dive Series

  1. NestFactory
  2. @Module and DynamicModule
  3. InstnaceLoader and Injector

@Module

NestJS uses the @Module Decorator when declaring a module. According to the official NestJS document, @Module is used to handle the metadata needed to manage the application structure.

A module is a class annotated with a @Module() decorator. The @Module() decorator provides metadata that Nest makes use of to organize the application structure.

Looking at the internal implementation of the @Module decorator in the NestJS source code, you can see that it adds metadata (ex, imports) provided as parameters to the target object.

// packages/common/decorators/modules/module.decorator.ts
export function Module(metadata: ModuleMetadata): ClassDecorator {
const propsKeys = Object.keys(metadata);
validateModuleKeys(propsKeys);

return (target: Function) => {
for (const property in metadata) {
if (metadata.hasOwnProperty(property)) {
Reflect.defineMetadata(property, (metadata as any)[property], target);
}
}
};
}t

Reflect

Reflect is a global object embedded in javascript that enables meta-programming by adding various metadata to javascript objects and their properties at runtime. More information about Reflect can be found in the proposal and API documents.

DependenciesScanner

Metadata registered through the @Module decorator is used by the DependenciesScanner to register information about the respective modules’ references (imports), dependencies (providers, controllers). This can be found in the scanForModules() and scanModulesForDependencies methods, which play a key role in the DependenciesScanner.

DependenciesScanner.scanModules()

// packages/core/scanner.ts
export class DependenciesScanner {
...
public async scanForModules(
moduleDefinition:
| ForwardReference
| Type<unknown>
| DynamicModule
| Promise<DynamicModule>,
scope: Type<unknown>[] = [],
ctxRegistry: (ForwardReference | DynamicModule | Type<unknown>)[] = [],
): Promise<Module[]> {
const moduleInstance = await this.insertModule(moduleDefinition, scope);
moduleDefinition =
moduleDefinition instanceof Promise
? await moduleDefinition
: moduleDefinition;
ctxRegistry.push(moduleDefinition);

if (this.isForwardReference(moduleDefinition)) {
moduleDefinition = (moduleDefinition as ForwardReference).forwardRef();
}

// ===========================================================
const modules = !this.isDynamicModule(
moduleDefinition as Type<any> | DynamicModule,
)

? this.reflectMetadata(
MODULE_METADATA.IMPORTS, // <<<<<<<<<<<<<<<<<<
moduleDefinition as Type<any>,
)
: [
...this.reflectMetadata(
MODULE_METADATA.IMPORTS, // <<<<<<<<<<<<<<<<<<
(moduleDefinition as DynamicModule).module,
),
...((moduleDefinition as DynamicModule).imports || []),
];
// ===========================================================
...
}

public reflectMetadata(metadataKey: string, metatype: Type<any>) {
return Reflect.getMetadata(metadataKey, metatype) || [];
}
}

DependenciesScanner.scanModulesForDependencies

// packages/core/scanner.ts
export class DependenciesScanner {
...
public async scanModulesForDependencies(
modules: Map<string, Module> = this.container.getModules(),
) {
for (const [token, { metatype }] of modules) {
await this.reflectImports(metatype, token, metatype.name);
this.reflectProviders(metatype, token); // <<<<<<<<<<<<<<<<<<
this.reflectControllers(metatype, token);
this.reflectExports(metatype, token);
}
}

public reflectProviders(module: Type<any>, token: string) {
const providers = [
// =========================================================
...this.reflectMetadata(MODULE_METADATA.PROVIDERS, module), // <<<<<<<<<<<<<<<<<<
// =========================================================
...this.container.getDynamicMetadataByToken(
token,
MODULE_METADATA.PROVIDERS as 'providers',
),
];
providers.forEach(provider => {
this.insertProvider(provider, token);
this.reflectDynamicMetadata(provider, token);
});
}

public reflectMetadata(metadataKey: string, metatype: Type<any>) {
return Reflect.getMetadata(metadataKey, metatype) || []; // <<<<<<<<<<<<<<<<<<
}
}

DynamicModule

In addition to @Module, NestJS provides DynamicModule which enables dynamic registration of modules. A deeper description of DynamicModule is outside the scope of this posting. A detailed description of the DynamicModule is available at https://docs.nestjs.com/fundamentals/dynamic-modules

// example of a dynamic module
import { Module, DynamicModule } from '@nestjs/common';
import { createDatabaseProviders } from './database.providers';
import { Connection } from './connection.provider';

@Module({
providers: [Connection],
})
export class DatabaseModule {
static forRoot(entities = [], options?): DynamicModule {
const providers = createDatabaseProviders(options, entities);
return {
module: DatabaseModule,
providers: providers,
exports: providers,
};
}
}

If you look at the implementation, you can see that DynamicModule is a form of additional declaration of dependency metadata to registered module objects. Modules registered through the Module (for convenience, we will call them StaticModule later in this post) store dependency information such as imports and controllers in the object’s metadata using Reflect. On the other hand, DynamicModule stores its additional metadata in the properties of the object instance in addition to the metadata the target StaticModule already has. Therefore, additional parsing is performed when registering DynamicModule .

// packages/common/interfaces/modules/dynamic-module.interface.ts
export interface DynamicModule extends ModuleMetadata {
module: Type<any>;
global?: boolean;
}
// packages/common/interfaces/modules/module-metadata.interface.ts
export interface ModuleMetadata {
imports?: Array<Type<any> | DynamicModule | Promise<DynamicModule> | ForwardReference>;
controllers?: Type<any>[];
providers?: Provider[];
exports?: Array<
| DynamicModule
| Promise<DynamicModule>
| string
| symbol
| Provider
| ForwardReference
| Abstract<any>
| Function
>;
}

NestContainer

To take a closer look at how DynamicModule is registered, let’s go back to the NestContainer that we covered in the previous post. Module registration in NestJS is done internally by calling the NestContainer.addModule(). The ModuleCompiler is responsible for parsing metadata of dynamically registered modules within NestContainer.

export class NestContainer {
public async addModule(
metatype: Type<any> | DynamicModule | Promise<DynamicModule>,
scope: Type<any>[],
): Promise<Module | undefined> {
if (!metatype) {
throw new UndefinedForwardRefException(scope);
}

// ==============================================================================
const { type, dynamicMetadata, token } = await this.moduleCompiler.compile(
metatype,
);
// ==============================================================================

if (this.modules.has(token)) {
return this.modules.get(token);
}
const moduleRef = new Module(type, this);
moduleRef.token = token;
this.modules.set(token, moduleRef);

await this.addDynamicMetadata(
token,
dynamicMetadata,
[].concat(scope, type),
);

if (this.isGlobalModule(type, dynamicMetadata)) {
this.addGlobalModule(moduleRef);
}
return moduleRef;
}
}
// packages/core/injector/compiler.ts
export class ModuleCompiler {
constructor(private readonly moduleTokenFactory = new ModuleTokenFactory()) {}

public async compile(
metatype: Type<any> | DynamicModule | Promise<DynamicModule>,
): Promise<ModuleFactory> {
const { type, dynamicMetadata } = this.extractMetadata(await metatype);
const token = this.moduleTokenFactory.create(type, dynamicMetadata);
return { type, dynamicMetadata, token };
}

public extractMetadata(metatype: Type<any> | DynamicModule): {
type: Type<any>;
dynamicMetadata?: Partial<DynamicModule> | undefined;
} {
if (!this.isDynamicModule(metatype)) {
return { type: metatype };
}
const { module: type, ...dynamicMetadata } = metatype;
return { type, dynamicMetadata };
}

public isDynamicModule(
module: Type<any> | DynamicModule,
): module is DynamicModule {
return !!(module as DynamicModule).module;
}
}

Afterwards, parsed metadata is added to dynamicModuleMetadataproperty. Finally, you can see that other modules that are imported from DynamicModule are registered recursively.

export class NestContainer {
public async addModule(
metatype: Type<any> | DynamicModule | Promise<DynamicModule>,
scope: Type<any>[],
): Promise<Module | undefined> {
if (!metatype) {
throw new UndefinedForwardRefException(scope);
}

const { type, dynamicMetadata, token } = await this.moduleCompiler.compile(
metatype,
);

if (this.modules.has(token)) {
return this.modules.get(token);
}
const moduleRef = new Module(type, this);
moduleRef.token = token;
this.modules.set(token, moduleRef);

// ==============================================================================
await this.addDynamicMetadata(
token,
dynamicMetadata,
[].concat(scope, type),
);
// ==============================================================================

if (this.isGlobalModule(type, dynamicMetadata)) {
this.addGlobalModule(moduleRef);
}
return moduleRef;
}

public async addDynamicMetadata(
token: string,
dynamicModuleMetadata: Partial<DynamicModule>,
scope: Type<any>[],
) {
if (!dynamicModuleMetadata) {
return;
}
this.dynamicModulesMetadata.set(token, dynamicModuleMetadata);

const { imports } = dynamicModuleMetadata;
await this.addDynamicModules(imports, scope);
}

public async addDynamicModules(modules: any[], scope: Type<any>[]) {
if (!modules) {
return;
}
await Promise.all(modules.map(module => this.addModule(module, scope)));
}
}

Wrap up

In this post, we covered the internal process of NestJS registering metadata for StaticModule and DynamicModule. However, metadata only describes the relationship between module-to-module or module-to-dependency-objects. In practice, the ability to create instances of dependency objects and manage their lifecycle is required to fully implement dependency injection. In the next post, I’m going to talk about InstanceLoader and Injector which play these roles in NestJS.

--

--