[NestJS Deep Dive] 01. NestFactory
Recently, I’ve started using NestJS at work. In the process, not understanding core parts of NestJS often made it difficult to NestJS fluently. So, I decided to take some time to take a close look at the NestJS framework for my personal studies and this is the first post of my NestJS deep dive series. As a first post of this series, I’d like to talk about NestFactory
class, which is the entry point for all NestJS applications.
NestJS Deep Dive Series
NestFactory
If you look at the NestJS official documentation, you can find the following code in the tutorial you’re facing for the first time.
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(3000);
}
bootstrap();
You can find NestFactory
reads `AppModule` to create an instance of `NestApplication` that contains all the logic of processing requests forwarded to the server. The goal of this posting is to understand what is happening in NestFactory.create()
.
NestFactory.create()
If you look at NestFactory.create()
, it is implemented as follows. Let’s take a closer look at each line.
// packages/core/nest-factory.ts
public async create<T extends INestApplication = INestApplication>(
module: any,
serverOrOptions?: AbstractHttpAdapter | NestApplicationOptions,
options?: NestApplicationOptions,
): Promise<T> {
const [httpServer, appOptions] = this.isHttpServer(serverOrOptions)
? [serverOrOptions, options]
: [this.createHttpAdapter(), serverOrOptions];
const applicationConfig = new ApplicationConfig();
const container = new NestContainer(applicationConfig);
this.setAbortOnError(serverOrOptions, options);
this.registerLoggerConfiguration(appOptions);
await this.initialize(module, container, applicationConfig, httpServer);
const instance = new NestApplication(
container,
httpServer,
applicationConfig,
appOptions,
);
const target = this.createNestInstance(instance);
return this.createAdapterProxy<T>(target, httpServer);
}
HttpAdapter
If you look at the first three lines,NestFactory
either uses the injected HttpAdatper
as it is, or in most cases creates a new instance of HttpAdapter
by default.
// packages/core/nest-factory.ts
export class NestFactory {
public async create<T extends INestApplication = INestApplication>(
module: any,
serverOrOptions?: AbstractHttpAdapter | NestApplicationOptions,
options?: NestApplicationOptions,
): Promise<T> {
// =======================================================
const [httpServer, appOptions] = this.isHttpServer(serverOrOptions)
? [serverOrOptions, options]
: [this.createHttpAdapter(), serverOrOptions];
// =======================================================
const applicationConfig = new ApplicationConfig();
const container = new NestContainer(applicationConfig);
this.setAbortOnError(serverOrOptions, options);
this.registerLoggerConfiguration(appOptions);
await this.initialize(module, container, applicationConfig, httpServer);
const instance = new NestApplication(
container,
httpServer,
applicationConfig,
appOptions,
);
const target = this.createNestInstance(instance);
return this.createAdapterProxy<T>(target, httpServer);
}
}
HttpAdapter
corresponds to the class implementing the AbsctractHttpAdapter
interface defined by NestJS Core. AbsctractHttpAdapter
is an abstract class that implements part of the HttpServer
interface that defines the required interfaces for processing http requests.
// packages/core/adapters/http-adapters.ts
export abstract class AbstractHttpAdapter<
TServer = any,
TRequest = any,
TResponse = any,
> implements HttpServer<TRequest, TResponse>
{
protected httpServer: TServer;
constructor(protected instance?: any) {}
public use(...args: any[]) {
return this.instance.use(...args);
}
public get(...args: any[]) {
return this.instance.get(...args);
}
public post(...args: any[]) {
return this.instance.post(...args);
}
...
abstract close();
abstract status(response: any, statusCode: number);
abstract reply(response: any, body: any, statusCode?: number);
abstract end(response: any, message?: string);
abstract render(response: any, view: string, options: any);
abstract redirect(response: any, statusCode: number, url: string);
}
One interesting part is that AbstractHttpAdapter
relies on the injected instance
property to implement core methods (ex, use
, get
, post
). I think some of you have already noticed that the names of the methods are familiar.
Yes, this method matches the method name used by express
to register middleware or handlers. In fact, NestFactory
uses express
internally to implement ExpressAdapter
as its default HttpAdapter
, which implements the AbstractHttpAdapter
interface. From this point of view, NestJS
can also be seen as a kind of “express-wrapper” framework. If you’re familiar with express
, it’s a pleasure.
// packages/platform-express/adapters/express-adapter.ts
export class ExpressAdapter extends AbstractHttpAdapter {
...
constructor(instance?: any) {
super(instance || express()); // << express!!
}
}
// packages/core/nest-factory.ts
export class NestFactoryStatic {
...
private createHttpAdapter<T = any>(httpServer?: T): AbstractHttpAdapter {
const { ExpressAdapter } = loadAdapter(
'@nestjs/platform-express',
'HTTP',
() => require('@nestjs/platform-express'),
);
return new ExpressAdapter(httpServer);
}
}
ApplicationConfig
Let’s move on to the next line. NestFactory
will then create an instance of ApplicationConfig
// packages/core/nest-factory.ts
export class NestFactory {
public async create<T extends INestApplication = INestApplication>(
module: any,
serverOrOptions?: AbstractHttpAdapter | NestApplicationOptions,
options?: NestApplicationOptions,
): Promise<T> {
const [httpServer, appOptions] = this.isHttpServer(serverOrOptions)
? [serverOrOptions, options]
: [this.createHttpAdapter(), serverOrOptions];
// =======================================================
const applicationConfig = new ApplicationConfig();
// =======================================================
const container = new NestContainer(applicationConfig);
this.setAbortOnError(serverOrOptions, options);
this.registerLoggerConfiguration(appOptions);
await this.initialize(module, container, applicationConfig, httpServer);
const instance = new NestApplication(
container,
httpServer,
applicationConfig,
appOptions,
);
const target = this.createNestInstance(instance);
return this.createAdapterProxy<T>(target, httpServer);
}
}
ApplicationConfig
corresponds to a relatively simple data object that stores the global middleware used by NestJS
. Although most of them are initially initialized to empty values, all global middleware is registered internally in ApplicationConfig
later on.
// packages/core/application-config.ts
export class ApplicationConfig {
private globalPrefix = '';
private globalPrefixOptions: GlobalPrefixOptions<ExcludeRouteMetadata> = {};
private globalPipes: Array<PipeTransform> = [];
private globalFilters: Array<ExceptionFilter> = [];
private globalInterceptors: Array<NestInterceptor> = [];
private globalGuards: Array<CanActivate> = [];
private versioningOptions: VersioningOptions;
...
}
NestContainer
Next, NestFactory
creates NestContainer
that plays a key role in the dependency injection (often called, DI
) in NestJS
. NestContainer
is an object that stores Module
classes that is essential for NestApplication
to operate.
// packages/core/nest-factory.ts
export class NestFactory {
public async create<T extends INestApplication = INestApplication>(
module: any,
serverOrOptions?: AbstractHttpAdapter | NestApplicationOptions,
options?: NestApplicationOptions,
): Promise<T> {
const [httpServer, appOptions] = this.isHttpServer(serverOrOptions)
? [serverOrOptions, options]
: [this.createHttpAdapter(), serverOrOptions];
const applicationConfig = new ApplicationConfig();
// =======================================================
const container = new NestContainer(applicationConfig);
// =======================================================
this.setAbortOnError(serverOrOptions, options);
this.registerLoggerConfiguration(appOptions);
await this.initialize(module, container, applicationConfig, httpServer);
const instance = new NestApplication(
container,
httpServer,
applicationConfig,
appOptions,
);
const target = this.createNestInstance(instance);
return this.createAdapterProxy<T>(target, httpServer);
}
}
NestContainer
internally has Module
, ModuleTokenFactory
, ModuleCompiler
, and ModulesContainer
. NestContainer
will then register the module via DependenciesScanner
. More details on this will be covered later.
// packages/core/injector/factory.ts
export class NestContainer {
private readonly globalModules = new Set<Module>();
private readonly moduleTokenFactory = new ModuleTokenFactory();
private readonly moduleCompiler = new ModuleCompiler(this.moduleTokenFactory);
private readonly modules = new ModulesContainer();
private readonly dynamicModulesMetadata = new Map<string, Partial<DynamicModule>>();
...
public addProvider(
provider: Provider,
token: string,
): string | symbol | Function {
const moduleRef = this.modules.get(token);
if (!provider) {
throw new CircularDependencyException(moduleRef?.metatype.name);
}
if (!moduleRef) {
throw new UnknownModuleException();
}
return moduleRef.addProvider(provider);
}
}
Module
The Module
class is an object that defines all dependencies (ex, Provider
, Controller
) contained in a single module. Modules that we typically register with @Module
decoration or through the forRoot()
method as DynamicModule
are managed as Module
classes within the NestJS
framework.
// packages/core/injector/module.ts
export class Module {
private readonly _id: string;
private readonly _imports = new Set<Module>();
private readonly _providers = new Map<
InstanceToken,
InstanceWrapper<Injectable>
>();
private readonly _injectables = new Map<
InstanceToken,
InstanceWrapper<Injectable>
>();
private readonly _middlewares = new Map<
InstanceToken,
InstanceWrapper<Injectable>
>();
private readonly _controllers = new Map<
InstanceToken,
InstanceWrapper<Controller>
>();
}
ModuleTokenFactory
ModuleTokenFactory
is a simple class that generates a token used as a key when registering and retrieving modules.
// packages/core/injector/module-token-factory.ts
export class ModuleTokenFactory {
public create(
metatype: Type<unknown>,
dynamicModuleMetadata?: Partial<DynamicModule> | undefined,
): string {
const moduleId = this.getModuleId(metatype);
const opaqueToken = {
id: moduleId,
module: this.getModuleName(metatype),
dynamic: this.getDynamicMetadataToken(dynamicModuleMetadata),
};
return hash(opaqueToken, { ignoreUnknown: true });
}
...
}
ModuleCompiler
NestContainer
separately manages the dependency related information of DynamicModule
. For this purpose, ModuleCompiler
can be considered as a utility class that performs the function of properly parsing the information in each module.
// packages/core/injector/compiler.ts
export class ModuleCompiler {
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 };
}
...
}
ModulesContainer
ModulesContainer
is a relatively simple class of Map<string, Module>
type that stores all modules. The key value will use the token generated by ModuleTokenFactory
// packages/core/injector/modules-container.ts
export class ModulesContainer extends Map<string, Module> {
private readonly _applicationId = uuid();
get applicationId(): string {
return this._applicationId;
}
}
initialize()
Next, NestFactory
calls the initialize()
method after a simple setup. The core role of the initialize()
method can be largely divided into the module registration process by DependenciesScanner
and the dependency object instance creation done by InstanceLoader
.
// packages/core/nest-factory.ts
export class NestFactory{
...
private async initialize(
module: any,
container: NestContainer,
config = new ApplicationConfig(),
httpServer: HttpServer = null,
) {
const instanceLoader = new InstanceLoader(container);
const metadataScanner = new MetadataScanner();
const dependenciesScanner = new DependenciesScanner(
container,
metadataScanner,
config,
);
container.setHttpAdapter(httpServer);
const teardown = this.abortOnError === false ? rethrow : undefined;
await httpServer?.init();
try {
this.logger.log(MESSAGES.APPLICATION_START);
await ExceptionsZone.asyncRun(
async () => {
await dependenciesScanner.scan(module);
await instanceLoader.createInstancesOfDependencies();
dependenciesScanner.applyApplicationProviders();
},
teardown,
this.autoFlushLogs,
);
} catch (e) {
this.handleInitializationError(e);
}
}
}
DependenciesScanner
The most important role of DependenciesScanner
is to traverse the module tree and register the modules in NestContaine
. In addition, DependenciesScanner
registers relationships between modules (edge
), manages information about dependencies linked to modules such as providers
and calculates the depth of modules node used when traversing modules topological order inside NestApplication
// packages/core/scanner.ts
export class DependenciesScanner {
...
public async scan(module: Type<any>) {
await this.registerCoreModule(); // register InternalCoreModule to NestContainer
await this.scanForModules(module); // registers all modules in NestContainer by traversing the Module Tree
await this.scanModulesForDependencies(); // add information related to the dependencies (imports, providers, controllers, exports) of registered modules through metadata parsing
this.calculateModulesDistance(); // register the depth information required to sort modules in topological order
this.addScopedEnhancersMetadata(); // register provider's Scope-related metadata additionally to the controller
this.container.bindGlobalScope(); // adding connection information to modules importing Global Modules
}
InstanceLoader
InstanceLoader
is responsible for sequentially creating prototype objects and instances of dependencies such as Provider
, Injectable
and Controller
of modules registered in NestContainer
. Creating a prototype is quite simple, but creating instances with complex dependency injection is relatively difficult. This function is executed by an internal class called Injector
. Injector
plays a key role in dependency injection, including determining the hierarchy of dependent objects, using callbacks to coordinate the order of instances being created, and properly inject them to create instances of all dependent objects. There is a lot of content to cover in this post, so we will find out more about Injector
in other posts later.
// packages/core/injector/instance-loader.ts
export class InstanceLoader {
protected readonly injector = new Injector();
public async createInstancesOfDependencies(
modules: Map<string, Module> = this.container.getModules(),
) {
this.createPrototypes(modules);
await this.createInstances(modules);
}
...
}
NestApplication
It’s finally the final step. NestFactory
creates an instance of NestApplication
after completing both the module and the dependency object instance registration process. Afterwards, the role of NestFactory
ends by wrapping it with Proxy
object for some functions such as error handling, method chaining, fallback and so on.
// packages/core/nest-factory.ts
export class NestFactory {
public async create<T extends INestApplication = INestApplication>(
module: any,
serverOrOptions?: AbstractHttpAdapter | NestApplicationOptions,
options?: NestApplicationOptions,
): Promise<T> {
const [httpServer, appOptions] = this.isHttpServer(serverOrOptions)
? [serverOrOptions, options]
: [this.createHttpAdapter(), serverOrOptions];
const applicationConfig = new ApplicationConfig();
const container = new NestContainer(applicationConfig);
this.setAbortOnError(serverOrOptions, options);
this.registerLoggerConfiguration(appOptions);
await this.initialize(module, container, applicationConfig, httpServer);
// =======================================================
const instance = new NestApplication(
container,
httpServer,
applicationConfig,
appOptions,
);
const target = this.createNestInstance(instance);
return this.createAdapterProxy<T>(target, httpServer);
// =======================================================
}
}
In this post, we briefly covered how NestApplication
, which contains all service logic is being created by NestFactory
. In the following post, I’m going to talk more about Modules
and h ow NestApplication
handles requests delivered to servers in more detail.