generator-kube-microservice-node
Version:
A k8s micro-service generator with deployment, service, Dockerfile. Built with express/mongo/redis
335 lines (251 loc) • 11.9 kB
Markdown
# generator-kube-microservice-node
[](#contributors)
[](https://conventionalcommits.org)
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:
```typescript
('/user')
export default class UserController {
(REFERENCES.UserService) private userService: UserService;
('/')
async getTenants( () res: Response) {
const result = await this.tenantService.find({ throwErrors: true });
res.status(OK).send(result);
}
('/:id')
async getUser( () res: Response, ('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.
```typescript
import { injectable } from 'inversify';
import { MongoService } from '../shared/class/MongoService';
import { UserInterface } from '../models/UserInterface';
import { UserSchema, UserModel } from '../models/UserModel';
()
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:
```typescript
('/user')
export default class UserController {
(REFERENCES.UserService) private userService: UserService;
(REFERENCES.RedisController) private redis: RedisController;
('/')
async getUsers( () res: Response) {
const result = await this.userService.find({});
res.status(OK).send(result);
}
('/:id')
async getUser( () res: Response, ('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:
```typescript
()
export default class UserService extends MongoService<UserInterface> {
(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
```typescript
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`
```typescript
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:
```typescript
export default class UserController {
(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 `.env`file (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:
```yaml
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](https://allcontributors.org/docs/en/emoji-key)):
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
<!-- prettier-ignore -->
<table>
<tr>
<td align="center"><a href="https://www.linkedin.com/in/vitor-diego/"><img src="https://avatars1.githubusercontent.com/u/15676011?v=4" width="100px;" alt="Vitor Die.go"/><br /><sub><b>Vitor Die.go</b></sub></a><br /><a href="https://github.com/e3Labs/generator-kube-microservice-node/issues?q=author%3Adiegofreemind" title="Bug reports">🐛</a> <a href="#ideas-diegofreemind" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center"><a href="https://github.com/Blira"><img src="https://avatars2.githubusercontent.com/u/43551066?v=4" width="100px;" alt="Bruno Lira"/><br /><sub><b>Bruno Lira</b></sub></a><br /><a href="https://github.com/e3Labs/generator-kube-microservice-node/commits?author=Blira" title="Code">💻</a> <a href="#maintenance-Blira" title="Maintenance">🚧</a></td>
</tr>
</table>
<!-- ALL-CONTRIBUTORS-LIST:END -->
This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!