Nocturnal Pajama Mutants

    generator-kube-microservice-node

    2.5.3 • Public • Published

    generator-kube-microservice-node

    All Contributors

    Conventional Commits

    A yeoman generator for nodejs micro services with TypeScript, Express, Mongoose, Redis and RabbitMQ.

    Why?

    This project is a boilerplate for extensible micro services written in TypeScript. Contains the minimum to instant deploy a service on a Kubernetes Cluster.

    Contents

    This template contains:

    • TypeScript
    • Dockerfile
    • Kubernetes deployment configuration (including Service k8s object)
    • TypeScript definition for mongo operators on controller functions
    • MongoService abstract class for all entities services
    • GenericException base for all exceptions
    • withException decorator to abstract error handling logic (used on generated controllers)
    • RemoteController class to handle axios requets
    • RabbitMQ consumers and producers logic
    • expressjs implementation with inversifyjs and inversify-express-utils

    Install

    • npm i -g yo
    • npm i -g generator-kube-microservice-node
    • yo kube-microservice-node
    • Follow the inscructions

    How to run

    • Run yarn dev to spawn a nodemon server watching source files
    • Create a .env file in root to handle all your secrets. Look at src/config/env.ts to see the default list of variables

    Usage

    Controllers

    Controllers of this boilerplate are handled by inversify-express-utils package.

    Here is a exemple:

    @controller('/user')
    export default class UserController {
      @inject(REFERENCES.UserService) private userService: UserService;
     
      @httpGet('/')
      @withException
      async getTenants(@response() res: Response) {
        const result = await this.tenantService.find({ throwErrors: true });
        res.status(OK).send(result);
      }
     
      @httpGet('/:id')
      @withException
      async getUser(@response() res: Response, @requestParam('id') id: string) {
        // Using Redis
        const [exception, result] = this.redis.withRedis({ key: 'getUser', expires: 10 }, () =>
          this.userService.findById({ id }),
        );
        if (!exception) {
          return res.status(exception.statusCode).send(exception.formatError())
        }
        return res.status(OK).send(result);
      }

    There's two types of response when using MongoService:

    • A result using Either<L, R>
    • The raw entity

    The two examples are described above.

    Everything is injected by inversify and the composition root lives in src/config/inversify.config.ts. Your entities controllers should be imported on src/config/inversify.config.ts, so inversify-express-utils can inject your controller on express routes.

    Inside the composition root, we import all controllers and inversifyjs takes care to setup our application (as seen on src/index.ts)

    Services

    The service layer extends the MongoService<T> which has all methods to handle the mongoose model.

    import { injectable } from 'inversify';
    import { MongoService } from '../shared/class/MongoService';
    import { UserInterface } from '../models/UserInterface';
    import { UserSchema, UserModel } from '../models/UserModel';
     
    @injectable()
    export default class UserService extends MongoService<UserInterface> {
      constructor() {
        /**
         * MongoService uses the Schema because if you change the default database while using some method from MongoService,
         * mongoose don't knows how to create the model schema for this non default database, so we help mongoose to do that
         */
        super(UserModel, UserSchema);
      }
    }

    Redis

    Redis connection occurs when you require redis into another class. Use like this:

    @controller('/user')
    export default class UserController {
      @inject(REFERENCES.UserService) private userService: UserService;
      @inject(REFERENCES.RedisController) private redis: RedisController;
     
      @httpGet('/')
      @withException
      async getUsers(@response() res: Response) {
        const result = await this.userService.find({});
        res.status(OK).send(result);
      }
     
      @httpGet('/:id')
      @withException
      async getUser(@response() res: Response, @requestParam('id') id: string) {
        // This method gets a entry from cache and set it if don't exist
        const result = this.redis.withRedis({ key: 'getUser', expires: 10 }, () =>
          this.userService.findById({ id, throwErrors: true }),
        );
        if (!result) {
          throw new EntityNotFoundException({ id });
        }
        res.status(OK).send(result);
      }

    RabbitMQ

    To use a consume/producer function for RabbitMQ, bootstrap the connection on your Service like this:

     
    @injectable()
    export default class UserService extends MongoService<UserInterface> {
     
      @inject(REFERENCES.EventBus) private eventBus: EventEmitter;
      private _channel: Channel;
      constructor() {
        super(UserModel, UserSchema);
        // Only connect to Rabbit when mongo is connected
        this.eventBus.on('mongoConnection', this._createRabbitMQChannelAndSetupQueue);
        // Reconnect to rabbitmq
        this.eventBus.on('reconnectRabbitMQ', this._createRabbitMQChannelAndSetupQueue);
      }
     
      /**
     * Creates a RabbitMQ Channel and setup the queue for this service
     */
      // Run this function on constructor
      private async _createRabbitMQChannelAndSetupQueue() {
        this._channel = await createRabbitMQChannel(env.rabbitmq_url);
        // Some consumer on ./src/queue/consumers
        consumeCreateUser(this._channel, this._consumeCreateUser);
      }
      /**
       * RabbitMQ Consumer CREATE_USER Function
       *
       * Creates a user
       * @param payload RabbitMQ ConsumeMessage type.
       */
      private _consumeCreateUser = async (payload: ConsumeMessage) => {
        const data = JSON.parse(payload.content.toString());
        /** DO SOMETHING  */
        this._channel.ack(payload); // sends a acknowledgement
      };
    }
     

    The producer is straight forward: just call the function that sends something to a queue (ex: ./src/queue/producers/)

    Exceptions

    All exceptions that are catch by src/server/middlewares/index.ts, have GenericException as they base.

    So, just continuing throw new errors based on GenericException.ts that express will catch and handle. (see src/shared/exceptions/ folder for default exceptions created)

    Service authorization

    In src/server/ you can find a Unauthorized.ts file that handles authorization logic of this service.

    Using this middleware, you should have another service with endpoint /auth that receives a JWToken via Authorization header.

    If that service responds with 200, you're authorized to procced with your request into this service.

    To use it, just insert into src/server/ServerFactory.ts a line containing this middleware

    import * as bodyParser from 'body-parser';
    import * as compression from 'compression';
    import * as cors from 'cors';
    import * as express from 'express';
    import { RouteNotFoundMiddleware, ExceptionMiddleware } from './middlewares';
    import Unauthorized from './Unauthorized';
     
    export default {
      initExternalMiddlewares(server: express.Application) {
        server.use(compression());
        server.use(bodyParser.json());
        server.use(cors());
      },
      initExceptionMiddlewares(server: express.Application) {
        // New Line!!!
        server.use(Unauthorized)
        server.use(RouteNotFoundMiddleware);
        server.use(ExceptionMiddleware);
      },
    };

    Dependency Injection

    This template uses inversifyjs to handle DI with a IoC container. The file that handles that is src/config/inversify.config.ts

    import '../entities/User/UserController';
    import '../shared/middlewares/HealthCheck';
     
    import { Container } from 'inversify';
     
    import REFERENCES from './inversify.references';
    import Connection from '../shared/class/Connection';
    import UserService from '../entities/User/UserService';
    import RemoteController from '../shared/class/RemoteController';
     
    const injectionContainer = new Container({ defaultScope: 'Singleton' });
     
    injectionContainer.bind(REFERENCES.Connection).to(Connection);
    injectionContainer.bind(REFERENCES.RemoteController).to(RemoteController);
    injectionContainer.bind(REFERENCES.UserService).to(UserService);
     
     
    export default injectionContainer;
     

    If your controller has another class dependency, inject the dependency onto your class like this:

    export default class UserController {
      @inject(REFERENCES.UserService) private userService: UserService;
    }

    Docker and Kubernetes

    To build a docker image, you have to build the project using npm run build and npm run build:webpack. Then, use npm run build:docker, and to publish, use npm run publish:docker. Remember to edit these commands if you use private repos.

    The Kubernetes deployment file (deployment.yaml), has a LivenessProbe that checks if the route /health returns 200. This route, pings to the database. If something goes wrong, your service will be restarted.

    The Service object in deployment.yaml file expose the Pod created by the Deployment to the world on port 80 and binding the port 3000 of the Pod to it.

    After configuring, you need to add the Service definition in a ingress controller of your k8s cluster.

    Since this template uses Kubernetes, the .dockerignore and Dockerfile files DOESN'T have a reference to .envfile (which, also is ignored on .gitignore file). The way to go about it is setting a envFrom field on deployment.yaml.

    Here is a example:

    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: user-service
    spec:
      replicas: 4
      selector:
        matchLabels:
          app: user-service
      template:
        metadata:
          labels:
            app: user-service
        spec:
          containers:
          name: user-service
            image: <some-image>
            ports:
              containerPort: 3000
            envFrom:
            configMapRef:
                name: env-config
            livenessProbe:
              initialDelaySeconds: 20
              periodSeconds: 5
              httpGet:
                path: /health
                port: 3000

    Contributing

    PR's and new issues are welcome. Anything you think that'll be great to this project will be discussed.

    Development

    Clone this repo, then, npm install and npm link. Now you can test this generator locally using yo command.

    Acknowledgements

    Many thanks for the folks that worked hard on:

    • inversifyjs (https://github.com/inversify/InversifyJS)
    • inversify-express-utils (https://github.com/inversify/inversify-express-utils)

    Without these libs, this boilerplate doesn't exists

    Contributors ✨

    Thanks goes to these wonderful people (emoji key):

    Vitor Die.go
    Vitor Die.go

    🐛 🤔
    Bruno Lira
    Bruno Lira

    💻 🚧

    This project follows the all-contributors specification. Contributions of any kind welcome!

    Install

    npm i generator-kube-microservice-node

    DownloadsWeekly Downloads

    1

    Version

    2.5.3

    License

    MIT

    Unpacked Size

    103 kB

    Total Files

    51

    Last publish

    Collaborators

    • wandersonalves