Hi everyone.
Last time, we covered how NestFactory builds NestApplication. This time, let’s look at how Module, one of NestJS’s core building blocks, gets registered into your application.
@Module
NestJS declares a module with the @Module decorator. The official docs describe @Module as the way Nest gathers the metadata it needs to organize the application’s structure. (Note: “module” here means something different from the internal Module class NestJS uses under the hood.)
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.
Look at the internal implementation of the @Module decorator in the NestJS source, and you’ll see it simply attaches the data you passed as parameters (imports and the rest) onto the target class as metadata.
// 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);
}
}
};
}
Reflect
Reflect is a global object built into JavaScript that enables metaprogramming: attaching arbitrary metadata to any object and its properties at runtime. You can read more in the proposal and the API docs.
DependenciesScanner
DependenciesScanner uses the metadata registered through @Module to record each module’s relationships (imports) and dependencies (providers, controllers, and so on). You’ll find this logic in its two core methods, scanForModules() and scanModulesForDependencies().
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
Beyond @Module, NestJS also offers DynamicModule, a way to configure a module dynamically at registration time. A deep dive into DynamicModule is out of scope for this post, but you can read the full explanation here.
// 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,
};
}
}
Look at how DynamicModule is implemented, and you’ll see it’s really just a way to declare additional dependency metadata on a registered module object. A module registered through @Module (let’s call it a StaticModule from here on) stores its dependency info (imports, controllers, and so on) as metadata via Reflect. A DynamicModule, on the other hand, stores extra information as instance properties on top of whatever a StaticModule already carries in its metadata. That means registering a DynamicModule requires an extra parsing step.
// 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 see how a DynamicModule actually gets registered, let’s revisit NestContainer from the last post. NestContainer is where module data actually lives. Registering a module internally means calling NestContainer.addModule(), and ModuleCompiler handles parsing the metadata for any dynamically registered module inside it.
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;
}
}
From here, the parsed DynamicModule metadata gets stored in the dynamicModuleMetadata property, as shown below. Notice, too, that any modules the DynamicModule imports get 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
This post covered how StaticModule and DynamicModule metadata gets registered internally. But metadata only describes relationships: module-to-module, or module-to-dependency. Actually performing dependency injection needs something more: creating instances of those dependency objects and managing their lifecycle. Next time, I’ll dig into InstanceLoader and Injector, the two classes responsible for that in NestJS.