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!

Package Sidebar

Install

npm i generator-kube-microservice-node

Weekly Downloads

0

Version

2.5.3

License

MIT

Unpacked Size

103 kB

Total Files

51

Last publish

Collaborators

  • wandersonalves