We write nest.js from scratch in typescript

Quoting from the documentation:

Nest provides an out-of-the-box application architecture that enables developers and teams to build highly verifiable, scalable, loosely coupled, and easily maintainable applications. The architecture is heavily inspired by Angular.

Nest is built on the basis of a design pattern – dependency injection. We will see how it is implemented in Nest and how it affects the rest of the code.

First, let’s look at the simplest code for starting a nestjs application from the documentation:

main.ts

import { NestFactory } from "@nestjs/core";
import { AppModule } from "./app.module";
 
async function bootstrap() {
    const app = await NestFactory.create(AppModule);
    await app.listen(3000);
}
 
bootstrap();

So. NestFactory the main class nest is where it all starts. His method create scans existing modules that are available in the dependency tree from the root AppModule, then scans the dependencies of the resulting modules in the form of controllers, services, and other modules, and adds them all to the container. Returns an application instance after scanning. When you run the listen method, the server is initialized and the routers created in the controller are registered, with the necessary callbacks for each, which are already stored in the container.

We will come to the creation of such functionality a little later, but first we will see what controller, provider and module we have.

app.controller.ts

import { Controller, Get, Post, Body, Param } from 'nestjs/common';
import { AppService } from './app.service';
 
@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}
 
  @Get()
  getHello(): string {
    return this.appService.getHello();
  }
 
  @Post('body/:id')
  recieveBody(@Body() data: { recieveData: string }, @Param('id') id: string) {
    return 'body: ' + data.recieveData + ' has been recieved and id: ' + 'id';
  }
}

What is interesting about this code is that when creating request methods, we simply attach decorators to functions that become callbacks for routers. We do not know when they are registered and how exactly, we do not need to think about the implementation, all we do is follow the given architecture. We say what we want to do, not how, that is, as Nest users, we take a declarative approach. Accordingly, we implement this functionality in this article.

There is also an AppService dependency that Nest implements itself. For us, this is a working code. Dependencies in Nest are resolved by type, and we’ll look at how.

app.service.ts

import { Injectable } from 'nestjs/common';
 
@Injectable()
export class AppService {
  getHello(): string {
    return 'Hello World!';
  }
}

Here we see the Injectable decorator, which, according to the description in the documentation, defines AppService as a class that the Nest container can manage, which is not entirely true. In fact, all it does is add metadata about the lifetime of the class. By default, this is the same as the application’s lifetime, and Nest itself does not recommend changing this behavior. Therefore, if you do not want to change this, then Injectable can be omitted. And the Nest container can manage it only if it is present in the providers of the module in which it is used.

app.module.ts

import { Module } from 'nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
 
@Module({
  imports: [],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

So. returning to main.tsimplement NestFactory Class.

It is worth mentioning that some auxiliary functions, like checking for null, etc., as well as the interface will be omitted in the article, but will be in the source code.

./core/nest-factory.ts

import { NestApplication } from "./nest-application";
import { NestContainer } from "./injector/container";
import { InstanceLoader } from "./injector/instance-loader";
import { DependenciesScanner } from "./scanner";
import { ExpressAdapter } from '../platform-express/express.adapter';
 
export class NestFactoryStatic {
    public async create(module: any) {
        const container = new NestContainer();
	  // Сканирует зависимости, создает экземпляры
	  // и внедряет их
        await this.initialize(module, container);
 
	  // Инициализирует http сервер и регистрирует роутеры
 	  // при запуске instance.listen(3000)
        const httpServer = new ExpressAdapter()
        container.setHttpAdapter(httpServer);
        const instance = new NestApplication(
            container,
            httpServer,
            applicationConfig,
        );
       
        return instance;
    }
 
    private async initialize(
        module: any,
        container: NestContainer,
    ) {
        const instanceLoader = new InstanceLoader(container)
        const dependenciesScanner = new DependenciesScanner(container);
 
        await dependenciesScanner.scan(module);
        await instanceLoader.createInstancesOfDependencies();
    }
}
 
 
/**
 * Используйте NestFactory для создания экземпляра приложения.
 *
 * ### Указание входного модуля
 *
 * Передайте требуемый *root module* (корневой модуль) для приложения
 * через параметр модуля. По соглашению он обычно называется
 * `AppModule`. Начиная с этого модуля Nest собирает граф
 * зависимостей и создает экземпляры классов, необходимых для запуска
 * вашего приложения.
 *
 * @publicApi
 */
export const NestFactory = new NestFactoryStatic();

So. From the code, we see that all existing modules are scanned first, as well as their dependencies, and then an http server is created. So now we are implementing the DependenciesScanner class. It will turn out a little more than the previous one, but let’s not be afraid, because there is nothing complicated, in fact, there.

./core/scanner.ts

import { MODULE_METADATA } from "../common/constants";
import { NestContainer } from "./injector/container";
import 'reflect-metadata';
import { Module } from "./injector/module";
 
export class DependenciesScanner {
 
    constructor(private readonly container: NestContainer) {}
 
    public async scan(module: any) {
        // Сначала сканирует все модули, которые есть в приложении, и добавляет их в контейнер
        await this.scanForModules(module);
        // После у каждого модуля сканирует зависимости, такие как Controllers и Providers
        await this.scanModulesForDependencies();
    }
 
    public async scanForModules(module: any) {
        // Добавляет модуль в контейнер и возвращает при этом его экземпляр
        const moduleInstance = await this.insertModule(module);
        // Получает модули, которые были импортированы в этот модуль в массив imports.
        // Так как AppModule - корневой модуль, то от него идет дерево модулей.
        const innerModules = [...this.reflectMetadata(moduleInstance, MODULE_METADATA.IMPORTS)];
 
        // Перебирает внутренние модули этого модуля, чтобы сделать с ними тоже самое.
        // То есть, происходит рекурсия.
        for (const [index, innerModule] of innerModules.entries()) {
            await this.scanForModules(innerModule)
        }
 
        return moduleInstance
    }
 
    /**
     * Добавляет модуль в контейнер
     */
    public async insertModule(module: any) {
        return this.container.addModule(module);
    }
 
 
    /**
     * Получает из контейнера все модули, и сканирует у них
     * зависимости, которые хранятся в reflect объекте.
     */
    public async scanModulesForDependencies() {
        const modules: Map<string, Module> = this.container.getModules();
 
        for (const [token, { metatype }] of modules) {
            await this.reflectAndAddImports(metatype, token);
            this.reflectAndAddProviders(metatype, token);
            this.reflectAndAddControllers(metatype, token);
        }
    }
 
    public async reflectAndAddImports(
        module: any,
        token: string,
    ) {
        // Получает по модулю imports зависимости и добавляет их в контейнер
        const modules = this.reflectMetadata(module, MODULE_METADATA.IMPORTS);
        for (const related of modules) {
            await this.container.addImport(related, token);
        }
    }
 
    public reflectAndAddProviders(
        module: any,
        token: string,
    ) {
        // Получает по модулю providers зависимости и добавляет их в контейнер
        const providers = this.reflectMetadata(module, MODULE_METADATA.PROVIDERS);
        providers.forEach((provider: any) =>
            this.container.addProvider(provider, token),
        );
    }
 
    public reflectAndAddControllers(module: any, token: string) {
        // Получает по модулю controllers зависимости и добавляет их в контейнер
        const controllers = this.reflectMetadata(module, MODULE_METADATA.CONTROLLERS);
        controllers.forEach((controller: any) =>
            this.container.addController(controller, token),
        );
    }
 
    /**
     * Метод, который получает нужные зависимости по модулю и ключу зависимостей.
     */
    public reflectMetadata(metatype: any, metadataKey: string) {
        return Reflect.getMetadata(metadataKey, metatype) || [];
    }
}

Looking at the code, we can see that modules and their dependencies are added to the container, and this is possible thanks to the two classes used here – NestContainer and Module. In fact, modules are stored in the container as instances of the Module class, and their dependencies, such as other modules, controllers, and providers, are stored in the Module, in data structures such as Map and Set. And the dependencies themselves, controllers and providers, are instances of the InstanceWrapper class.

The Module and InstanceWrapper classes in our implementation are pretty simple, especially the second one, so let’s implement our container first.

./core/injector/container.ts

import { Module } from "./module";
import { ModuleTokenFactory } from "./module-token-factory";
import { AbstractHttpAdapter } from "../adapters";
 
export class NestContainer {
    private readonly modules = new Map<string, Module>();
    private readonly moduleTokenFactory = new ModuleTokenFactory();
    private httpAdapter: AbstractHttpAdapter | undefined;
 
    /**
     * Создает экземпляр класса Module и сохраняет его в контейнер
     */
    public async addModule(module: any) {
        // Создает токен модуля, который будет являться его ключом Map,
        // который и будет использоваться для проверки и получения этого модуля.
        const token = this.moduleTokenFactory.create(module);
 
        if (this.modules.has(module.name)) {
            return;
        }
 
        const moduleRef = new Module(module);
        moduleRef.token = token;
        this.modules.set(token, moduleRef);
 
        return moduleRef;
    }
 
    /**
     * Возвращает все модули, для сканирования зависимостей,
     * создания экземпляров этих зависимостей, и для использования в качестве callbacks
     * при создании роутеров его контроллеров, с разрешенными зависимостями.
     */
    public getModules(): Map<string, Module> {
        return this.modules;
    }
 
    /**
     * Контейнер также устанавливает и хранит единственный экземпляр http сервера,
     * в нашем случае express. Этот метод вызывается в классе NestFactory.
     */
    public setHttpAdapter(httpAdapter: any) {
        this.httpAdapter = httpAdapter;
    }
 
    /**
     * Будет вызван при создании роутеров в классе RouterExplorer.
     */
    public getHttpAdapterRef() {
        return this.httpAdapter;
    }    
 
    /**
     * При сканировании зависимостей для полученных модулей в DependenciesScanner,
     * у них также берется токен, по которому здесь находится модуль,
     * и с помощью своего метода добавляет к себе импортированный модуль.
     */
    public async addImport(
        relatedModule: any,
        token: string,
    ) {
        if (!this.modules.has(token)) {
            return;
        }
        const moduleRef = this.modules.get(token);
        if (!moduleRef) {
            throw Error('MODULE NOT EXIST')
        }
 
        const related = this.modules.get(relatedModule.name);
        if (!related) {
            throw Error('RELATED MODULE NOT EXIST')
        }
        moduleRef.addRelatedModule(related);
    }
 
    /**
     * Также как и для импортированных модулей, подобная функциональность
     * работает и для провайдеров.
     */
    public addProvider(provider: any, token: string) {
        if (!this.modules.has(token)) {
            throw new Error('Module not found.');
        }
        const moduleRef = this.modules.get(token);
        if (!moduleRef) {
            throw Error('MODULE NOT EXIST')
        }
        moduleRef.addProvider(provider)
    }
 
    /**
     * Также как и для импортированных модулей, подобная функциональность
     * работает и для контроллеров.
     */
    public addController(controller: any, token: string) {
        if (!this.modules.has(token)) {
            throw new Error('Module not found.');
        }
        const moduleRef = this.modules.get(token);
        if (!moduleRef) {
            throw Error('MODULE NOT EXIST')
        }
        moduleRef.addController(controller);
    }
}

We also saw here the ModuleTokenFactory class, which creates a token by which the module is stored. In fact, you can get by with the usual creation of a unique id, for example, using the uuid package. Therefore, you can not pay much attention to it, but, for those who are interested, here is the closest implementation of this class to the implementation of Nest, only somewhat simplified.

./core/injector/module-token-factory.ts

import hash from 'object-hash';
import { v4 as uuid } from 'uuid';
import { Type } from '../../common/interfaces/type.interface';
 
export class ModuleTokenFactory {
    // Здесь хранятся данные о том, какие модули уже были отсканированы.
    // На случай того, если один модуль является зависимостью у нескольких,
    // чтобы не было дубликатов.
    private readonly moduleIdsCashe = new WeakMap<Type<unknown>, string>()
 
    public create(metatype: Type<unknown>): string {
        const moduleId = this.getModuleId(metatype);
        const opaqueToken = {
            id: moduleId,
            module: this.getModuleName(metatype),
        };
        return hash(opaqueToken, { ignoreUnknown: true });
    }
 
    public getModuleId(metatype: Type<unknown>): string {
        let moduleId = this.moduleIdsCashe.get(metatype);
        if (moduleId) {
            return moduleId;
        }
        moduleId = uuid();
        this.moduleIdsCashe.set(metatype, moduleId);
        return moduleId;
    }
 
    public getModuleName(metatype: Type<any>): string {
        return metatype.name;
    }
}

Now let’s look at the Module class.

./core/injector/module.ts

import { InstanceWrapper } from "./instance-wrapper";
import { randomStringGenerator } from "../../common/utils/random-string-generator.util";
 
export class Module {
    private readonly _imports = new Set<Module>();
    private readonly _providers = new Map<any, InstanceWrapper>();
    private readonly _controllers = new Map<string, InstanceWrapper>();
 
    private _token: string | undefined;
 
    constructor(
        private readonly module: any,
    ) {}
 
    get providers(): Map<string, any> {
        return this._providers;
    }
 
    get controllers(): Map<string, any> {
        return this._controllers;
    }
 
    get metatype() {
        return this.module;
    }
 
    get token() {
        return this._token!;
    }
 
    set token(token: string) {
        this._token = token;
    }
 
    public addProvider(provider: any) {
        this._providers.set(
            provider.name,
            new InstanceWrapper({
              name: provider.name,
              metatype: provider,
              instance: null,
            }),
        )
    }
 
    public addController(controller: any) {
        this._controllers.set(
            controller.name,
            new InstanceWrapper({
              name: controller.name,
              metatype: controller,
              instance: null,
            }),
        );
 
        this.assignControllerUniqueId(controller);
    }
 
    public assignControllerUniqueId(controller: any) {
        Object.defineProperty(controller, 'CONTROLLER_ID', {
          enumerable: false,
          writable: false,
          configurable: true,
          value: randomStringGenerator(),
        });
    }
 
    public addRelatedModule(module: Module) {
        this._imports.add(module);
    }
}

Comments are unnecessary here. All it does is store the dependencies of a particular module, the module itself, and its token.

Now consider an even simpler InstanceWrapper class.

./core/injector/instance-wrapper.ts

import { Type } from '../../common/interfaces/type.interface';
 
export class InstanceWrapper<T = any> {
    public readonly name: string;
    public metatype: Type<T> | Function;
    public instance: any;
    public isResolved = false
 
    constructor(metadata: any) {
        Object.assign(this, metadata);
        this.instance = metadata.instance;
        this.metatype = metadata.metatype;
        this.name = metadata.name
    }
}

When it is created, null is assigned to instance. In the future, for example, if the controller has a dependency in the form of a provider in its constructor, then during dependency injection, an instance of this provider will already be created, and when the controller is instantiated, it will be added to its constructor. This is how dependencies are resolved. In fact, this is what we will continue to do.

We now have the functionality to scan modules and their dependencies. Modules are added to the container, stored according to the created token in the form of the Module class, in which they are all represented, and store their dependencies, which are in the reflect object, in the Map and Set data structures.

Now let’s go back to the NestContainer class and take a look at its initialize method.

private async initialize(
        module: Module,
        container: NestContainer,
    ) {
        const instanceLoader = new InstanceLoader(container)
        const dependenciesScanner = new DependenciesScanner(container);
 
        await dependenciesScanner.scan(module);
        await instanceLoader.createInstancesOfDependencies();

to its initialize method

Now that we’ve scanned the modules, we need to instantiate their dependencies. Therefore, we will now implement the InstanceLoader class.

./core/injector/instance-loader.ts

import { NestContainer } from "./container";
import { Injector } from "./injector";
import { Module } from "./module";
 
export class InstanceLoader {
    private readonly injector = new Injector();
 
    constructor(private readonly container: NestContainer) {}
 
    public async createInstancesOfDependencies() {
        const modules = this.container.getModules();
 
        await this.createInstances(modules);
    }
 
    /**
     * Сначала создаются экземпляры провайдеров,
     * потому что если они являются зависимостями контроллеров,
     * при создании экземпляров для контроллеров, они уже должны
     * существовать.
     */
    private async createInstances(modules: Map<string, Module>) {
        await Promise.all(
            [...modules.values()].map(async module => {
                await this.createInstancesOfProviders(module);
                await this.createInstancesOfControllers(module);
            })
        )
    }
 
    private async createInstancesOfProviders(module: Module) {
        const { providers } = module;
        const wrappers = [...providers.values()];
        await Promise.all(
            wrappers.map(item => this.injector.loadProvider(item, module)),
        )
    }
 
    private async createInstancesOfControllers(module: Module) {
        const { controllers } = module;
        const wrappers = [...controllers.values()];
        await Promise.all(
            wrappers.map(item => this.injector.loadControllers(item, module)),
        )
    }
}

It’s not a hard class either. Everything it does calls methods of the Injector class. What is worth noting here, which is already written in the comment to the createInstances method, is that the created instances of providers will be added to the constructors of the corresponding controllers when they are instantiated.

Now let’s look at the Injector class, which is a little more interesting than the rest, and which does the dependency injection.

./core/injector/injector.ts

import { Module } from "./module";
import { InstanceWrapper } from './instance-wrapper';
import { Type } from  '../../common/interfaces/type.interface';
 
export class Injector {
 
    public async loadInstance<T>(
        wrapper: InstanceWrapper<T>,
        collection: Map<string, InstanceWrapper>,
        moduleRef: Module,
    ) {
        const { name } = wrapper;
 
        const targetWrapper = collection.get(name);
        if (!targetWrapper) {
            throw Error('TARGET WRAPPER NOT FOUNDED')
        }
        const callback = async (instances: unknown[]) => {
            await this.instantiateClass(
                instances,
                wrapper,
                targetWrapper,
            );
        }
        await this.resolveConstructorParams<T>(
            wrapper,
            moduleRef,
            callback,
          );
    }
 
    public async loadProvider(
        wrapper: any,
        moduleRef: Module,
    ) {
        const providers = moduleRef.providers;
        await this.loadInstance<any>(
            wrapper,
            providers,
            moduleRef,
        );
    }
 
    public async loadControllers(
        wrapper: any,
        moduleRef: Module,
    ) {
        const controllers = moduleRef.controllers;
        await this.loadInstance<any>(
            wrapper,
            controllers,
            moduleRef,
        );
    }
 
    /**
     * design:paramtypes создается автоматически объектом reflect
     * для зависимостей, указанных в конструкторе класса.
     * Как видно, если провайдеру нужно разрешить зависимости,
     * то они также должны быть провайдерами.
     * callback, как видно из метода loadInstance, вызывает метод
     * instantiateClass для найденных зависимостей в виде провайдеров.
     */
    public async resolveConstructorParams<T>(
        wrapper: InstanceWrapper<T>,
        moduleRef: Module,
        callback: (args: unknown[]) => void | Promise<void>,
    ) {
        const dependencies = Reflect.getMetadata('design:paramtypes', wrapper.metatype)
   
        const resolveParam = async (param: Function, index: number) => {
          try {
            let providers = moduleRef.providers
            const paramWrapper = providers.get(param.name);
            return paramWrapper?.instance
          } catch (err) {
              throw err;
          }
        };
        const instances = dependencies ? await Promise.all(dependencies.map(resolveParam)) : [];
        await callback(instances);
    }
 
    /**
     * Создает экземпляр зависимости, которая хранится в InstanceLoader,
     * как metatype, с ее зависимостями, которые являются провайдерами,
     * и добавляет этот экземпляр в instance поле класса InstanceLoader,
     * для дальнейшего извлечения при создании роутеров.
     */
    public async instantiateClass<T = any>(
        instances: any[],
        wrapper: InstanceWrapper,
        targetMetatype: InstanceWrapper,
    ): Promise<T> {
        const { metatype } = wrapper;
 
        targetMetatype.instance = instances
            ? new (metatype as Type<any>)(...instances)
            : new (metatype as Type<any>)();
       
        return targetMetatype.instance;
    }
}

Fine. Now we have scanned modules, and instantiated dependencies. Go ahead.

Now let’s go back to NestFactory, namely to its create method.

public async create(module: Module) {
        const applicationConfig = new ApplicationConfig();
        const container = new NestContainer();
        await this.initialize(module, container);
 
        const httpServer = new ExpressAdapter()
        container.setHttpAdapter(httpServer);
        const instance = new NestApplication(
            container,
            httpServer,
            applicationConfig,
        );
       
        return instance;

Here we have an ExpressAdapter class that inherits from the AbstractHttpAdapter class. That is, a design pattern known as an adapter is used here. Optionally, you can also create a FastifyAdapter class to use fastify instead of express. This is how it was done in nest, but here we will take express because of its greater prevalence.

Let’s look at the AbstractHttpAdapter first.

./core/adapters/http-adapter.ts

import { HttpServer } from "../../common/interfaces/http-server.interface";
 
export abstract class AbstractHttpAdapter<
  TServer = any,
  TRequest = any,
  TResponse = any
> implements HttpServer<TRequest, TResponse> {
  protected httpServer: TServer | undefined;
 
  constructor(protected readonly 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);
  }
 
  public listen(port: any) {
    return this.instance.listen(port);
  }
 
  public getHttpServer(): TServer {
    return this.httpServer as TServer;
  }
 
  public setHttpServer(httpServer: TServer) {
    this.httpServer = httpServer;
  }
 
  public getInstance<T = any>(): T {
    return this.instance as T;
  }
 
  abstract initHttpServer(): any;
  abstract reply(response: any, body: any, statusCode?: number): any;
  abstract registerBodyParser(prefix?: string): any;
}

We see that it implements several common http server methods. To simplify the code, our nest will only have two http methods, namely post and get.

And this is the interface that the adapter implements

interface HttpServer<TRequest = any, TResponse = any> {
    reply(response: any, body: any, statusCode?: number): any;
    get(handler: RequestHandler<TRequest, TResponse>): any;
    get(path: string, handler: RequestHandler<TRequest, TResponse>): any;
    post(handler: RequestHandler<TRequest, TResponse>): any;
    post(path: string, handler: RequestHandler<TRequest, TResponse>): any;
    listen(port: number | string): any;
 
    getInstance(): any;
    getHttpServer(): any;
    initHttpServer(): void;
    registerBodyParser(): void
}

Now let’s look at the ExpressAdapter class

./platform-express/express.adapter.ts

import { AbstractHttpAdapter } from '../core/adapters';
import { isNil, isObject } from '../common/utils/shared.utils'
import express from 'express';
import * as http from 'http';
import {
  json as bodyParserJson,
  urlencoded as bodyParserUrlencoded,
} from 'body-parser';
 
export class ExpressAdapter extends AbstractHttpAdapter {
 
    constructor() {
      super(express());
    }
 
    /**
     * Является response методом. С помощью него отправляются все данные.
     */
    public reply(response: any, body: any) {
        if (isNil(body)) {
          return response.send();
        }
   
        return isObject(body) ? response.json(body) : response.send(String(body));
    }
 
    /**
     * Запускает сервер на выборном порте
     */
    public listen(port: any) {
        return this.httpServer.listen(port);
    }
 
 
    public registerBodyParser() {
      const parserMiddleware = {
        jsonParser: bodyParserJson(),
        urlencodedParser: bodyParserUrlencoded({ extended: true }),
      };
      Object.keys(parserMiddleware)
        .forEach((parserKey: any) => this.use((parserMiddleware as any)[parserKey]));
      }
 
    public initHttpServer() {
        this.httpServer = http.createServer(this.getInstance());
    }
}

Actually, the launch and configuration of express is implemented here. In the constructor, in the super method, the express instance is passed to the AbstractHttpAdapter, from which the post, get and use methods will be called.

Now, back to NestFactory again,

public async create(module: Module) {
        const container = new NestContainer();
        await this.initialize(module, container);
 
        const httpServer = new ExpressAdapter()
        container.setHttpAdapter(httpServer);
        const instance = new NestApplication(
            container,
            httpServer,
        );
       
        return instance;
    }

we need to implement the NestApplication class, which is an instance of the entire Nest application. It is from it that the listen method is called,

async function bootstrap() {
    const app = await NestFactory.create(AppModule);
    await app.listen(3000);
}

which starts the application.

./core/nest-application.ts

import { HttpServer } from '../common/interfaces/http-server.interface';
import { Resolver } from './router/interfaces/resolver.interface';
import { addLeadingSlash } from '../common/utils/shared.utils';
import { NestContainer } from './injector/container';
import { RoutesResolver } from './router/routes-resolver';
 
export class NestApplication {
    private readonly routesResolver: Resolver;
    public httpServer: any;
 
    constructor(
        private readonly container: NestContainer,
        private readonly httpAdapter: HttpServer,
    ) {
        this.registerHttpServer();
 
        this.routesResolver = new RoutesResolver(
            this.container,
        );
    }
 
    public registerHttpServer() {
        this.httpServer = this.createServer();
    }
 
    /**
     * Начинает процесс инициализации выбранного http сервера
     */
    public createServer<T = any>(): T {
        this.httpAdapter.initHttpServer();
        return this.httpAdapter.getHttpServer() as T;
    }
 
    public async init(): Promise<this> {
        this.httpAdapter.registerBodyParser();
        await this.registerRouter();
        return this;
    }
 
    /**
     * Метод, с помощью которого запускается приложение Nest.
     * Он запускает процесс инициализации http сервера, регистрации
     * созданных роутеров, и запуска сервера на выбранном порте.
     */
    public async listen(port: number | string) {
        await this.init();
        this.httpAdapter.listen(port);
        return this.httpServer;
    }
 
    /**
     * Метод, который запускает регистрацию роутеров,
     * которые были созданы с помощью декораторов http методов,
     * таких как post и get.
     */
    public async registerRouter() {
        const prefix = ''
        const basePath = addLeadingSlash(prefix);
        this.routesResolver.resolve(this.httpAdapter, basePath);
    }
}

And that brings us to routers, namely the RoutesResolver class.

./core/router/routes-resolver.ts

import { NestContainer } from '../injector/container';
import { Resolver } from '../router/interfaces/resolver.interface';
import { MODULE_PATH } from '../../common/constants';
import { HttpServer } from '../../common/interfaces/http-server.interface';
import { InstanceWrapper } from '../injector/instance-wrapper';
import { RouterExplorer } from './router-explorer';
 
export class RoutesResolver implements Resolver {
    private readonly routerExplorer: RouterExplorer;
 
    constructor(
        private readonly container: NestContainer,
      ) {
        this.routerExplorer = new RouterExplorer(
            this.container,
        );
    }
 
    /**
     * Для каждого модуля сначала находит базовый путь, который
     * указывается в декораторе Module,
     * и передает его и контроллеры в метод registerRouters
     */
    public resolve(applicationRef: any, basePath: string): void {
        const modules = this.container.getModules();
        modules.forEach(({ controllers, metatype }) => {
            let path = metatype ? this.getModulePathMetadata(metatype) : undefined;
            path = path ? basePath + path : basePath;
            this.registerRouters(controllers, metatype.name, path, applicationRef);
        });
    }
 
    /**
     * Для каждого контроллера в модуле, запускает метод explore
     * класса routerExplorer, который отвечает за всю логику
     * регистрации роутеров
     */
    public registerRouters(
        routes: Map<string, InstanceWrapper<any>>,
        moduleName: string,
        basePath: string,
        applicationRef: HttpServer,
      ) {
        routes.forEach(instanceWrapper => {
          const { metatype } = instanceWrapper;
   
          // Находит путь для декоратора контроллера, например @Controller('cats')
          const paths = this.routerExplorer.extractRouterPath(
            metatype as any,
            basePath,
          );
   
          // Если путь был передан как @Controllers('cats'), то будет вызвано один раз.
          // Дело в том, что reflect возвращает массив
          paths.forEach(path => {
            this.routerExplorer.explore(
              instanceWrapper,
              moduleName,
              applicationRef,
              path,
            );
          });
        });
      }
 
    private getModulePathMetadata(metatype: object): string | undefined {
        return Reflect.getMetadata(MODULE_PATH, metatype);
    }
}

The code above makes it so that the explore method of the RouterExplorer class is called for each controller. The RouterExplorer class implements the basic logic for registering routers. It creates http methods, adds controllers as their callbacks, binds those controllers to the module space it resides in, and implements the response and handling functionality for requests.

./core/router/routes-explorer.ts

import { NestContainer } from '../injector/container';
import { RouterProxyCallback } from './router-proxy';
import { addLeadingSlash } from '../../common/utils/shared.utils';
import { Type } from '../../common/interfaces/type.interface';
import { Controller } from '../../common/interfaces/controller.interface';
import { PATH_METADATA, METHOD_METADATA, ROUTE_ARGS_METADATA, PARAMTYPES_METADATA } from '../../common/constants';
import { RequestMethod } from '../../common/enums/request-method.enum';
import { HttpServer } from '../../common/interfaces/http-server.interface';
import { InstanceWrapper } from '../injector/instance-wrapper';
import { RouterMethodFactory } from '../helpers/router-method-factory';
import {
  isConstructor,
  isFunction,
  isString,
} from '../../common/utils/shared.utils';
import { RouteParamtypes } from '../../common/enums/route-paramtypes.enum';
 
 
export interface RoutePathProperties {
    path: string[];
    requestMethod: RequestMethod;
    targetCallback: RouterProxyCallback;
    methodName: string;
  }
 
export class RouterExplorer {
    private readonly routerMethodFactory = new RouterMethodFactory();
 
    constructor (
        private readonly container: NestContainer,
    ) {
    }    
 
    public explore<T extends HttpServer = any>(
        instanceWrapper: InstanceWrapper,
        module: string,
        router: T,
        basePath: string,
      ) {
        const { instance } = instanceWrapper;
        const routePaths: RoutePathProperties[] = this.scanForPaths(instance);
       
        // Для каждого метода контроллера запускает регистрацию роутеров
        (routePaths || []).forEach((pathProperties: any) => {
            this.applyCallbackToRouter(
              router,
              pathProperties,
              instanceWrapper,
              module,
              basePath,
            );
        })
    }
 
    /**
     * Метод, который сканирует контроллер, и находит у него методы
     * запроса с определенными путями, например метод, на который
     * навешен декоратор @post('add_to_database').
     * В таком случае эта функция возвращает массив методов контроллера
     * с путями, телами этих методов, методом request и именами, которые
     * получаются в методе exploreMethodMetadata
     */
    public scanForPaths(
        instance: Controller,
      ): RoutePathProperties[] {
        const instancePrototype = Object.getPrototypeOf(instance);
        let methodNames = Object.getOwnPropertyNames(instancePrototype);
 
        const isMethod = (prop: string) => {
          const descriptor = Object.getOwnPropertyDescriptor(instancePrototype, prop);
          if (descriptor?.set || descriptor?.get) {
            return false;
          }
          return !isConstructor(prop) && isFunction(instancePrototype[prop]);
        };
   
        return methodNames.filter(isMethod).map(method => this.exploreMethodMetadata(instance, instancePrototype, method))
    }
 
    /**
     * Для определенного метода контроллера возвращает его свойства,
     * для метода scanForPaths
     */
    public exploreMethodMetadata(
      instance: Controller,
      prototype: object,
      methodName: string,
    ): RoutePathProperties {
      const instanceCallback = (instance as any)[methodName];
      const prototypeCallback = (prototype as any)[methodName];
      const routePath = Reflect.getMetadata(PATH_METADATA, prototypeCallback);
 
      const requestMethod: RequestMethod = Reflect.getMetadata(
        METHOD_METADATA,
        prototypeCallback,
      );
      const path = isString(routePath)
        ? [addLeadingSlash(routePath)]
        : routePath.map((p: string) => addLeadingSlash(p));
      return {
        path,
        requestMethod,
        targetCallback: instanceCallback,
        methodName,
      };
    }
 
    private applyCallbackToRouter<T extends HttpServer>(
        router: T,
        pathProperties: RoutePathProperties,
        instanceWrapper: InstanceWrapper,
        moduleKey: string,
        basePath: string,
      ) {
        const {
          path: paths,
          requestMethod,
          targetCallback,
          methodName,
        } = pathProperties;
        const { instance } = instanceWrapper;
        // Получает определенный http метод
        const routerMethod = this.routerMethodFactory
          .get(router, requestMethod)
          .bind(router);
   
        // Создает callback для определенного метода
        const handler = this.createCallbackProxy(
          instance,
          targetCallback,
          methodName,
        );
   
        // Если декоратор используется как @Post('add_to_database'),
        // то будет вызвано один раз для этого пути.
        paths.forEach(path => {
          const fullPath = this.stripEndSlash(basePath) + path;
          // Региструет http метод. Сопоставляет путь метода, и его callback,
          // полученный из контроллера. Ответ же производится reply методом,
          // реализованным в классе ExpressAdapter
          routerMethod(this.stripEndSlash(fullPath) || '/', handler);
        });
    }
 
    public stripEndSlash(str: string) {
      return str[str.length - 1] === '/' ? str.slice(0, str.length - 1) : str;
    }
 
    public createCallbackProxy(
      instance: Controller,
      callback: (...args: any[]) => unknown,
      methodName: string,
    ) {
      // Достает ключи данных запроса указанных ранее в декораторах @Body() и @Param()
      const metadata = Reflect.getMetadata(ROUTE_ARGS_METADATA, instance.constructor, methodName) || {};
      const keys = Object.keys(metadata);
      const argsLength = Math.max(...keys.map(key => metadata[key].index)) + 1
     
      // Извлеченные данные из request, такие как тело и параметры запроса.
      const paramsOptions = this.exchangeKeysForValues(keys, metadata);
 
      const fnApplyParams = this.resolveParamsOptions(paramsOptions)
      const handler = <TRequest, TResponse>(
        args: any[],
        req: TRequest,
        res: TResponse,
        next: Function,
      ) => async () => {
        // так как args это объект, а не примитивная переменная,
        // то он передается по ссылке, а не по значению,
        // поэтому он изменяется, и после вызова fnApplyParams,
        // в args хранятся аргументы, полученные из request
        fnApplyParams && (await fnApplyParams(args, req, res, next));
        // Здесь мы привязываем один из методов контроллера,
        // например, добавление данных в базу данных, и аргументы из request,
        // и теперь он может ими управлять, как и задумано
        return callback.apply(instance, args);
      };
      const targetCallback = async <TRequest, TResponse>(
          req: TRequest,
          res: TResponse,
          next: Function,
        ) => {
          // Заполняется undefined для дальнейшего изменения реальными данными
          // из request
          const args = Array.apply(null, { argsLength } as any).fill(undefined);
          // result это экземпляр контроллера с пространством данных аргументов
          // из request
          const result = await handler(args, req, res, next)()
          const applicationRef = this.container.getHttpAdapterRef()
          if(!applicationRef) {
            throw new Error(`Http server not created`)
          }
          return await applicationRef.reply(res, result);
        }
      return async <TRequest, TResponse>(
        req: TRequest,
        res: TResponse,
        next: () => void,
      ) => {
        try {
          await targetCallback(req, res, next);
        } catch (e) {
          throw e
        }
      };
    }
 
    /**
     * extractValue здесь это метод exchangeKeyForValue.
     * И ему передается request, для извлечения данных запроса
     */
    public resolveParamsOptions(paramsOptions: any) {
      const resolveFn = async (args: any, req: any, res: any, next: any) => {
        const resolveParamValue = async (param: any) => {
          const { index, extractValue } = param;
          const value = extractValue(req, res, next);
          args[index] = value
        }
        await Promise.all(paramsOptions.map(resolveParamValue));
      }
      return paramsOptions && paramsOptions.length ? resolveFn : null;
    }
 
    /**
     * Перебирает ключи данных запроса для вызова для каждого
     * метода exchangeKeyForValue, который достанет соответствующие данные,
     * которые были определены ранее в декораторах @Body() и @Param(),
     * из request.
     */
    public exchangeKeysForValues(
      keys: string[],
      metadata: Record<number, any>,
    ): any[] {
      return keys.map((key: any) => {
        const { index, data } = metadata[key];
        const numericType = Number(key.split(':')[0]);
        const extractValue = <TRequest, TResponse>(
          req: TRequest,
          res: TResponse,
          next: Function,
        ) =>
          this.exchangeKeyForValue(numericType, data, {
            req,
            res,
            next,
        });
        return { index, extractValue, type: numericType, data }
      })
    }
 
    /**
     * Проверяет чему соответствует ключ данных, телу или параметрам запроса.
     * Это определяется в соответствующих декораторах @Body() и @Param().
     * И теперь, когда запрос на соответствующий api выполнен, мы пытаемся
     * достать их из request, если они были переданы.
     */
    public exchangeKeyForValue<
      TRequest extends Record<string, any> = any,
      TResponse = any,
      TResult = any
    >(
      key: RouteParamtypes | string,
      data: string | object | any,
      { req, res, next }: { req: TRequest; res: TResponse; next: Function },
    ): TResult | null {
      switch (key) {
        case RouteParamtypes.BODY:
          return data && req.body ? req.body[data] : req.body;
        case RouteParamtypes.PARAM:
          return data ? req.params[data] : req.params;
        default:
          return null;
      }
    }
 
    public extractRouterPath(metatype: Type<Controller>, prefix = ''): string[] {
        let path = Reflect.getMetadata(PATH_METADATA, metatype);
   
        if (Array.isArray(path)) {
          path = path.map(p => prefix + addLeadingSlash(p));
        } else {
          path = [prefix + addLeadingSlash(path)];
        }
   
        return path.map((p: string) => addLeadingSlash(p));
    }
}

What should be added to this is that in the applyCallbackToRouter method, the RouterMethodFactory class is used to get the http method, which, in fact, has only one method

./core/helpers/router-method-factory.ts

import { HttpServer } from '../../common/interfaces/http-server.interface';
import { RequestMethod } from '../../common/enums/request-method.enum';
 
export class RouterMethodFactory {
  public get(target: HttpServer, requestMethod: RequestMethod): Function {
    switch (requestMethod) {
      case RequestMethod.POST:
        return target.post;
      default: {
        return target.get;
      }
    }
  }
}

Well. If you’re still here, congratulations! We wrote the entire core of our mini Nest framework. Now, all that’s left is to write the decorators that we use to write the Nest app as users.

Let’s start with the @Module() decorator, and first look at an example of its use from the documentation

import { Module } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';
 
@Module({
  controllers: [CatsController],
  providers: [CatsService],
})
export class CatsModule {}

We see that the metadata is specified as decorator parameters, now let’s implement it.

./common/decorators/module.decorator.ts

import { MODULE_METADATA as metadataConstants } from '../constants';
 
const metadataKeys = [
  metadataConstants.IMPORTS,
  metadataConstants.EXPORTS,
  metadataConstants.CONTROLLERS,
  metadataConstants.PROVIDERS,
];
 
/**
 * Проверяет, чтобы были указаны только правильные массивы,
 * соответствующие metadataKeys
 */
export function validateModuleKeys(keys: string[]) {
  const validateKey = (key: string) => {
    if (metadataKeys.includes(key)) {
      return;
    }
    throw new Error(`NOT INVALID KEY: ${key}`);
  };
  keys.forEach(validateKey);
}
 
/**
 * Сохраняет зависимости в объект Reflect.
 * Где property название одной из зависимости,
 * например controllers. Именно благодаря этому,
 * у нас есть возможность извлекать данные после.
 */
export function Module(metadata: any): 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);
      }
    }
  };
}

Pretty difficult, right? Indeed, decorators are one of the rather simple parts of Nest.

Now let’s look at the @Controller() decorator, which, all it does is preserve the base path of the controller, because the controller itself is already stored in Reflect by the module in which it is used.

./common/decorators/controller.decorator.ts

import { PATH_METADATA } from "../constants";
import { isUndefined } from "../utils/shared.utils";
 
export function Controller(
  prefix?: string,
): ClassDecorator {
  const defaultPath="/";
 
  const path = isUndefined(prefix) ? defaultPath : prefix
 
  return (target: object) => {
    Reflect.defineMetadata(PATH_METADATA, path, target);
  };
}

Remember the @Injectable() decorator that supposedly marks the class as a provider? As already written above, it only sets the lifetime of the provider. A class is marked as a provider only if it is passed to the providers array of the corresponding module. And although we have not implemented the ability to change the lifetime for the provider, but for completeness, we will still consider this decorator.

./common/decorators/injectable.decorator.ts

import { SCOPE_OPTIONS_METADATA } from '../constants';
 
export enum Scope {
    DEFAULT,
    TRANSIENT,
    REQUEST,
}
 
export interface ScopeOptions {
    scope?: Scope;
}
 
export type InjectableOptions = ScopeOptions;
 
export function Injectable(options?: InjectableOptions): ClassDecorator {
    return (target: object) => {
      Reflect.defineMetadata(SCOPE_OPTIONS_METADATA, options, target);
    };
}

We now have only four decorators to implement, for the request data, namely @Body() and @Param(), and for the http methods, @Post() and @Get().

Let’s look at the first two first.

./common/decorators/route-params.decorator.ts

import { ROUTE_ARGS_METADATA } from "../constants";
import { RouteParamtypes } from "../enums/route-paramtypes.enum";
import { isNil, isString } from "../utils/shared.utils";
 
/**
 * Здесь используется неизменяемость данных, для того, чтобы
 * использовать один метод для нескольких типов запроса.
 */
const createPipesRouteParamDecorator = (paramtype: RouteParamtypes) => (
    data?: any,
  ): ParameterDecorator => (target, key, index) => {
    const hasParamData = isNil(data) || isString(data);
    const paramData = hasParamData ? data : undefined;
    const args =
      Reflect.getMetadata(ROUTE_ARGS_METADATA, target.constructor, key) || {};
 
    // Где paramtype это body или param, а index его
    // положение в параметрах функции, где находится декоратор,
    // для правильного присвоения после получения из request
    Reflect.defineMetadata(
      ROUTE_ARGS_METADATA,
      {
        ...args,
        [`${paramtype}:${index}`]: {
          index,
          data: paramData,
        },
      },
      target.constructor,
      key,
    );
};
 
export function Body(
    property?: string,
  ): ParameterDecorator {
    return createPipesRouteParamDecorator(RouteParamtypes.BODY)(
      property,
    );
}
 
export function Param(
    property?: string,
  ): ParameterDecorator {
    return createPipesRouteParamDecorator(RouteParamtypes.PARAM)(
      property,
    );
}

And finally, the post and get decorators, which store their paths and request methods in the Reflect object for certain controller methods.

./common/decorators/request-mapping.decorator.ts

import { METHOD_METADATA, PATH_METADATA } from '../constants';
import { RequestMethod } from '../enums/request-method.enum';
 
export interface RequestMappingMetadata {
  path?: string | string[];
  method?: RequestMethod;
}
 
const defaultMetadata = {
  [PATH_METADATA]: '/',
  [METHOD_METADATA]: RequestMethod.GET,
};
 
export const RequestMapping = (
  metadata: RequestMappingMetadata = defaultMetadata,
): MethodDecorator => {
  const pathMetadata = metadata[PATH_METADATA];
  const path = pathMetadata && pathMetadata.length ? pathMetadata : '/';
  const requestMethod = metadata[METHOD_METADATA] || RequestMethod.GET;
 
  return (
    target: object,
    key: string | symbol,
    descriptor: TypedPropertyDescriptor<any>,
  ) => {
    Reflect.defineMetadata(PATH_METADATA, path, descriptor.value);
    Reflect.defineMetadata(METHOD_METADATA, requestMethod, descriptor.value);
    return descriptor;
  };
};
 
const createMappingDecorator = (method: RequestMethod) => (
  path?: string | string[],
): MethodDecorator => {
  return RequestMapping({
    [PATH_METADATA]: path,
    [METHOD_METADATA]: method,
  });
};
 
/**
 * Обработчик маршрута (метод) Decorator. Направляет запросы HTTP POST по указанному пути.
 *
 * @publicApi
 */
export const Post = createMappingDecorator(RequestMethod.POST);
 
/**
 * Обработчик маршрута (метод) Decorator. Направляет запросы HTTP GET по указанному пути.
 *
 * @publicApi
 */
export const Get = createMappingDecorator(RequestMethod.GET);

Good job, our mini Nest is ready!

Now we can create a project-view directory at the level of other nest directories, and write a simple application

./project-view/main.ts

import { NestFactory } from '../core';
import { AppModule } from './app.module';
 
async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  await app.listen(3000);
}
bootstrap();

./project-view/app.controller.ts

import { Controller, Get, Post, Body, Param } from '../common';
import { AppService } from './app.service';
 
@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}
 
  @Get()
  getHello(): string {
    return this.appService.getHello();
  }
 
  @Post('body/:id')
  recieveBody(@Body() data: any, @Param('id') id: string) {
    return 'body: ' + data.data + ' has been received and id: ${id}';
  }
}

./project-view/app.service.ts

import { Injectable } from '../common';
 
@Injectable()
export class AppService {
  getHello(): string {
    return 'Hello World!';
  }
}

./project-view/app.module.ts

import { Module } from '../common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
 
@Module({
  imports: [],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

After that, initialize the typescript project by creating a tsconfig.json file using the command

tsc --init

and set it up like this

{
  "compilerOptions": {
    "target": "es2017",
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "module": "commonjs",
    "outDir": "./",
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "skipLibCheck": true,
  },
  "include": ["packages/**/*", "integration/**/*", "./core/", "./common/", "./project-view/", "./platform-express/"],
  "exclude": ["node_modules", "**/*.spec.ts"]
}

Now we can compile typescript to js with the following command

tsc --build

go to our custom application directory

cd project-view

and run the compiled input file

node main.js

You can check the result, for example via postman, and play around with the body and params for the post request.

Now you know Nest a little better 🙂

Github repository with all code

Contacts for communication:

Mail – keith.la.00@gmail.com

Telegram – @NLavrenov00

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *