preface

Recently the author has been developing nodeJS program with Nestjs.

Nestjs is a NodeJS framework built on Express (the default) or Fastify, which provides programming paradigms such as AOP (aspect oriented) and DI (dependency injection) with the help of Typescript and decorator.

Today we’ll show you how to implement simple dependency injection for Express using Typescript decorators.

Create a project

Initialization project:

NPM install -g typescript // Initialize the project NPM init -y TSC --initCopy the code

Install dependencies:

yarn add express
yarn add @types/express -D
Copy the code

Also we need to set the experimentalDecorators option in tsconfig to True to use the decorator:

{
  "compilerOptions": {
    "experimentalDecorators": true,}}Copy the code

The Controller decorator

The use of Express is familiar to many of you:

const express = require('express')
const app = express()
const port = 3000

app.get('/main/hello'.(req, res) = > res.send('Hello World! '))

app.listen(port, () = > console.log(`Example app listening on port ${port}! `))
Copy the code

After the transformation, the equivalent formula to be obtained is:

// index.ts
start({
  controllers: [MainController],
  port: 3000});// controllers.ts
@Controller('main')
export class MainController {
  @Get('hello')
  hello(req, res) {
    res.send('Hello World! '); }}Copy the code

Setting metadata

To set and retrieve metadata, we will install the reflect-metadata library (see syntax proposals and git repositories for details) :

yarn add reflect-metadata -D
Copy the code

We will use the reflect.definemetadata and reflect.getMetadata provided to access the metadata.

DefineMetadata and getMetadata are used on class and method decorators as follows:

Reflect.defineMetadata(key, value, target); / / class
Reflect.defineMetadata(key, value, target, propertyKey); / / method

Reflect.getMetadata(key, target); / / class
Reflect.getMetadata(key, target, propertyKey); / / method
Copy the code

We can then implement Controller and Get decorators to store metadata about request paths and HTTP methods:

// framework.ts
export function Controller(rootPath: string = ' ') {
  return (target) = > {
    Reflect.defineMetadata('ROOT_PATH', rootPath, target); }}function getControllerDecorator(method) {
  return (path: string = ' ') = > {
    return (target, propertyKey: string) = > {
      Reflect.defineMetadata('HTTP_METHOD', method, target, propertyKey);
      Reflect.defineMetadata('PATH', path, target, propertyKey); }}};export const Get = getControllerDecorator('get');
Copy the code

Getting metadata

After that, implement the start method:

// framework.ts
import * as express from 'express';

export const app = express();

export function start({ controllers, port = 3000 }: IConfig) {
  for (const cls of controllers) {
    const rootPath = Reflect.getMetadata('ROOT_PATH', cls);
    const instance = new cls();
    for (const propertyKey of Object.getOwnPropertyNames(cls.prototype)) {
      const method = Reflect.getMetadata('HTTP_METHOD', instance, propertyKey);
      const path = Reflect.getMetadata('PATH', instance, propertyKey);
      if(! method)continue;
      app[method](`${rootPath}/${path}`, instance[propertyKey].bind(instance));
    }
  }
  app.listen(port, () = > console.log(`Example app listening on port ${port}! `));
}
Copy the code

The above code is also relatively easy to understand. We iterate through an array of Controllers, each of which is a Controller class decorated with the Controller decorator. From that class we Get the path prefix. After creating an instance of the class, we iterate over the methods of each instance and register them with the Express instance if they are decorated with a Get, Post, or other decorator.

Dependency injection

Above we have implemented the Controller decorator, but not dependency injection.

We want dependency injection to work like this:

// controller.ts
@Controller('/math')
export class MathController {
  constructor(
    private readonly math: MathService,
  ) {}

  @Get('add')
  add(req, res) {
    res.send({ result: this.math.add(1.2)}); }}// service.ts
@Provider(a)export class MathService {
  add(a, b) {
    returna + b; }}Copy the code

Obviously, in our implementation of the start method, we need to modify the new CLS () part of the code to inject the dependency instance correctly.

So the question is, how do we know what the controller depends on?

That’s where emitDecoratorMetadata comes in. Modify tsconfig.json to set emitDecoratorMetadata to true:

{
  "compilerOptions": {
    "emitDecoratorMetadata": true,}}Copy the code

Write a Provider decorator, compile it with TSC, and then compile it with controller.js. You can find code like this:

MathController = __decorate([
    framework_1.Controller('/math'),
    __metadata("design:paramtypes", [service_1.MathService])
], MathController);
Copy the code

Wow, ts handles the dependency list for us and stores it in the metadata Design: Paramtypes.

Next, we just need to get the class from this metadata and generate the instance:

// framework.ts
const providerMap: WeakMap<any.Object> = new WeakMap(a);export function Provider() {
  return (target) = > {
    providerMap.set(target, null);
  };
}

function getInstance(cls) {
  const dependencies = (Reflect.getMetadata('design:paramtypes', cls) || []).map(cls= > {
    const instance = providerMap.get(cls);
    if (instance === null) providerMap.set(cls, getInstance(cls));
    return providerMap.get(cls);
  });
  return newcls(... dependencies); }export function start({ controllers, port = 3000 }: IConfig) {
  for (const cls of controllers) {
    constinstance = getInstance(cls); . } app.listen(port,() = > console.log(`Example app listening on port ${port}! `));
}
Copy the code

The getInstance method is used to handle dependencies and generate instances. GetInstance is called internally recursively so that a Provider can rely on other providers. At the same time, we use a class and instance WeakMap here, so that each Provider is a singleton mode. We can modify this behavior by passing parameters in the Provider decorator, which we won’t expand here.

After the