• Stars
    star
    27
  • Rank 878,409 (Top 18 %)
  • Language
    TypeScript
  • License
    Do What The F*ck ...
  • Created 10 months ago
  • Updated 10 months ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Building a nestjs like framework on top of express under 180 lines

Making your own NestJS

It can be a little overwhelming when using NestJS for the first time. Let's try to understand how some of it's components work.

Before you start

I am assuming that you are familiar with NestJS and the features it provides. Most of the features that feels like black magic is achieved with decorators and experimental metadata API. Make sure you have basic understanding of these.

I've set up this project with necessary config to run the working snippets I am about to share. You can put them in the playground.ts file and run them with the command npm run playground. Once you are done playing around with the snippets, you can see all of them coming together in the lib.ts file.

Defining routes

The key here is that, decorators can attach metadata to classes and methods, and those metadata can be accessed later on runtime. Let's dive right in.

import 'reflect-metadata';

const PATH_KEY = Symbol('path');
const HTTP_METHOD_KEY = Symbol('method');

/**
 * Post is a decorator factory that takes a path and returns a decorator.
 * That decorator attaches the path and the HTTP method post to the class method it is applied to.
 **/
function Post(path: string) {
  return function (target: any, key: string) {
    Reflect.defineMetadata(PATH_KEY, path, target, key);
    Reflect.defineMetadata(HTTP_METHOD_KEY, 'post', target, key);
  };
}
/* 👆 these are codes that the framework might provide */

/* So user can write something like this */
class AuthRoute {
  @Post('/login')
  async login() {
    return 'login success';
  }
}

/* Then the framework can use the class to create the actual routes */
function createApp(ControllerCls: any) {
  // first get all the properties of that class
  const properties = Object.getOwnPropertyNames(ControllerCls.prototype);
  properties
    .filter(
      (
        method // keep the ones that as HTTP method metadata
      ) => Reflect.hasOwnMetadata(HTTP_METHOD_KEY, ControllerCls.prototype, method)
    )
    .forEach(method => {
      const path = Reflect.getMetadata(PATH_KEY, ControllerCls.prototype, method);
      const httpMethod = Reflect.getMetadata(
        HTTP_METHOD_KEY,
        ControllerCls.prototype,
        method
      );
      console.log(`Mapping: ${httpMethod.toUpperCase()} ${path}`);
      // now that we have access to the method name and path at runtime,
      // these could be attached to an express app
    });
}

createApp(AuthRoute);

Note that, using symbol is not mandatory here, could just use plain strings for the keys.

Dependency injection

The basic idea of dependency injection is that instead of instantiating the dependencies of a class in the constructor, you pass the dependencies to the constructor. The mystical thing that NestJS does here is, you can define the dependencies in the constructor with shorthand express like constructor(private service: Service) and NestJS will do the instantiating and pass it down to the constructor for you. Let's see how something like that is possible.

Again metadata API comes into play. The parameters of a constructors is available with the design:paramtypes key in the metadata. The catch is that, the class has to be decorated with at least one decorator. Otherwise typescript will not record the parameter data while transpiling to runnable javascript. This is where the @Injectable() decorator comes into play. You might have noticed that NestJS will have you decorate the controllers with the Controller() decorator even when no controller prefix is required. This is because all classes need to be decorated with at least one decorator in order for them to be instantiated with correct params.

import 'reflect-metadata';

function Injectable() {
  return function (target: any) {}; // it doesn't have to do anything
}

class UserRepository {
  async findUser() {
    return 'user exists';
  }
}

@Injectable()
class AuthService {
  constructor(private readonly userRepository: UserRepository) {}
  login() {
    return this.userRepository.findUser();
  }
}

function instantiate(ProviderCls: any) {
  const params = Reflect.getMetadata('design:paramtypes', ProviderCls).map(
    DependencyCls => new DependencyCls()
  );

  const provider = new ProviderCls(...params);

  provider.login().then(console.log);
}

instantiate(AuthService);

Note that, it didn't need to be a decorator factory, since it is not taking params. We could define it like function Injectable(target: any) {} then it could be used like @Injectable, without the braces. Just making it look like the other decorators.

What about passing data down to the route handlers?

With a similar technique, parameters can be decorated to indicate what kind of data is needed in that parameter, and then the framework can pass down the appropriate data, taking form the underlying platform.

import 'reflect-metadata';

const HTTP_METHOD_KEY = Symbol('method');
const PATH_KEY = Symbol('path');
const PARAMS_META_KEY = Symbol('paramsMeta');

// just like the first snippet
function Get(path: string) {
  return function (target: any, key: string) {
    Reflect.defineMetadata(PATH_KEY, path, target, key);
    Reflect.defineMetadata(HTTP_METHOD_KEY, 'get', target, key);
  };
}

// decorator to indicate that data is required from route parameter
export function Param(key: string) {
  return function (target: any, methodName: string, index: number) {
    const paramsMeta = Reflect.getMetadata(PARAMS_META_KEY, target, methodName) ?? {};
    paramsMeta[index] = { key, type: 'route_param' };
    Reflect.defineMetadata(PARAMS_META_KEY, paramsMeta, target, methodName);
  };
}

class AuthRoute {
  @Get('/profile/:id')
  async profile(@Param('id') id: string) {
    return `user: ${id}`;
  }
}

function createApp(ControllerCls: any) {
  Object.getOwnPropertyNames(ControllerCls.prototype)
    .filter(method =>
      Reflect.hasOwnMetadata(HTTP_METHOD_KEY, ControllerCls.prototype, method)
    )
    .forEach(method => {
      const PARAM_DATA = { id: '123' }; // could get from req.params

      const paramsMeta =
        Reflect.getMetadata(PARAMS_META_KEY, ControllerCls.prototype, method) ?? {};

      const paramsToPass = Reflect.getMetadata(
        'design:paramtypes',
        ControllerCls.prototype,
        method
      ).map((_, index) => {
        const { key, type } = paramsMeta[index];
        if (type === 'route_param') return PARAM_DATA[key];
        return null;
      });
      ControllerCls.prototype[method](...paramsToPass).then(console.log);
    });
}

createApp(AuthRoute);

Putting it all together

I've tried to keep the snippets as short and easy to understand as possible. You can see the lib.ts where I've put them together with actual express. And in the index.ts there is a working web app using this 'framework'. Their you have it, a nestjs-like framework on top of express under 180 lines of code. It doesn't handle a lot of things of course, but it is a good starting point to understand how a framework like NestJS works under the hood.

Thanks for reading. Leave a star if you liked it.

More Repositories

1

cfviz

Visualizes user data from codeforces.com using the official API
JavaScript
1,028
star
2

dot-connect

Recreation of a childhood game on mern-stack
JavaScript
32
star
3

cs

Files related to my CS self study
Java
18
star
4

gold-price

Daily updated Gold price history in Bangladesh. Visualised with per gram and per bhori prices.
TypeScript
9
star
5

ABPBloodBank

A blood donor management app with react native and firebase
JavaScript
8
star
6

psbook

সমস্যা ও সমাধান বই - ১ এর উপর অনুশীলন ( Practice code for Problem and Solutions Book - 1 by Mahbubul Hasan )
C++
7
star
7

fire-adda

A simple react project to demonstrate firebase
JavaScript
4
star
8

psbookprogress

This is a small web app to track progress of the book সমস্যা ও সমাধান বই - ১ by @shanto86
Python
4
star
9

package-cloud

Vue app that creates word cloud of the packages you've used in your JavaScript projects hosted on GitHub
JavaScript
4
star
10

blog

My blog and portfolio
JavaScript
3
star
11

Dingulu

An android app that helps you know yourself better
JavaScript
2
star
12

unsplash-dbg

Use unsplash.com images as desktop background
Python
2
star
13

MakeThemFallTheGame

This is a simple game made with Processing and JBox2D.
Processing
1
star
14

react-native-animation-gesture-playground

Code of my playing around with react native gesture and animation
TypeScript
1
star
15

name-the-code

A quiz app where you have to guess the data structure or algorithm reading the code. Built with SSR (Next.js)
TypeScript
1
star
16

mqtt-vs-socketio

HTML
1
star
17

Bani

A messenger bot built with python web.py
Python
1
star
18

lightoj-solutions

LightOJ solutions scraped with scrapy
Java
1
star
19

pdf-viwer-demo

CSS
1
star
20

strava-personal-heatmap

JavaScript
1
star