Giter Site home page Giter Site logo

sjsakib / own-nestjs Goto Github PK

View Code? Open in Web Editor NEW
28.0 2.0 1.0 34 KB

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

Home Page: https://medium.com/@sjsakib/making-your-own-nestjs-73991d125be3

License: Do What The F*ck You Want To Public License

TypeScript 100.00%
decorators nestjs nodejs reflect-metadata typescript expressjs

own-nestjs's Introduction

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.

own-nestjs's People

Contributors

sjsakib avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

Forkers

farhadjaman

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.