generator-kube-microservice-node v2.5.3
generator-kube-microservice-node
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
Servicek8s object) - TypeScript definition for mongo operators on controller functions
MongoServiceabstract class for all entities servicesGenericExceptionbase for all exceptionswithExceptiondecorator to abstract error handling logic (used on generated controllers)RemoteControllerclass to handleaxiosrequetsRabbitMQconsumers and producers logicexpressjsimplementation withinversifyjsandinversify-express-utils
Install
npm i -g yonpm i -g generator-kube-microservice-nodeyo kube-microservice-node- Follow the inscructions
How to run
- Run
yarn devto spawn a nodemon server watching source files - Create a .env file in root to handle all your secrets. Look at
src/config/env.tsto 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: 3000Contributing
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):
This project follows the all-contributors specification. Contributions of any kind welcome!
5 years ago
5 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago