Moleculer
Moleculer is a fast & powerful microservices framework for NodeJS (>= v6.x).
Please do not use this in production since it's still under heavy development!
What's included
- Promise-based functions
- request-reply concept
- event bus system
- supports middlewares
- multiple services on a node/server
- built-in caching solution (memory, Redis)
- multiple transporters (NATS, MQTT, Redis)
- load balanced requests (round-robin, random)
- every nodes are equal, no master/leader node
- auto discovery services
- parameter validation
- request timeout handling with fallback response
- health monitoring & statistics
- supports versioned services (run different versions of the service)
Table of content
Installation
$ npm install moleculer --save
or
$ yarn add moleculer
Quick start
Simple service & call actions locally
const { ServiceBroker } = require("moleculer");
let broker = new ServiceBroker({
logger: console
});
broker.createService({
name: "math",
actions: {
add(ctx) {
return Number(ctx.params.a) + Number(ctx.params.b);
},
sub(ctx) {
return Number(ctx.params.a) - Number(ctx.params.b);
}
}
});
broker.start();
broker.call("math.add", { a: 5, b: 3 })
.then(res => console.log("5 + 3 =", res));
broker.call("math.sub", { a: 9, b: 2 })
.then(res => console.log("9 - 2 =", res))
.catch(err => console.error(`Error occured! ${err.message}`));
broker.call("math.add", { a: 3, b: 5})
.then(res => broker.call("math.sub", { a: res, b: 2 }))
.then(res => console.log("3 + 5 - 2 =", res));
Try it on Runkit
Main modules
ServiceBroker
The ServiceBroker
is the main component of Moleculer. It handles services & events, calls actions and communicates with remote nodes. You need to create an instance of ServiceBroker
on every node.
Create broker
Create broker with default settings
let { ServiceBroker } = require("moleculer");
let broker = new ServiceBroker();
Create broker with custom settings
let { ServiceBroker } = require("moleculer");
let broker = new ServiceBroker({
logger: console,
logLevel: "info"
});
Create with transporter
let { ServiceBroker, NatsTransporter } = require("moleculer");
let broker = new ServiceBroker({
nodeID: "node-1",
transporter: new NatsTransporter(),
logger: console,
logLevel: "debug",
requestTimeout: 5 * 1000,
requestRetry: 3
});
Create with cacher
let ServiceBroker = require("moleculer").ServiceBroker;
let MemoryCacher = require("moleculer").Cachers.Memory;
let broker = new ServiceBroker({
cacher: new MemoryCacher(),
logger: console,
logLevel: {
"*": "warn",
"CACHER": "debug"
}
});
Constructor options
All available options:
{
nodeID: null,
logger: null,
logLevel: "info",
transporter: null,
requestTimeout: 5 * 1000,
requestRetry: 0,
heartbeatInterval: 10,
heartbeatTimeout: 30,
cacher: null,
metrics: false,
metricsSendInterval: 5 * 1000,
statistics: false,
validation: true,
internalActions: true
ServiceFactory: null,
ContextFactory: null
}
Name | Type | Default | Description |
---|
nodeID | String | Computer name | This is the ID of node. It identifies a node in the cluster when there are many nodes. |
logger | Object | null | Logger class. During development you can set to console . In production you can set an external logger e.g. winston or pino |
logLevel | String or Object | info | Level of logging (debug, info, warn, error) |
transporter | Object | null | Instance of transporter. Required if you have 2 or more nodes. Internal transporters: NatsTransporter |
requestTimeout | Number | 5000 | Timeout of request in milliseconds. If the request is timed out, broker will throw a RequestTimeout error. Disable: 0 |
requestRetry | Number | 0 | Count of retry of request. If the request is timed out, broker will try to call again. |
cacher | Object | null | Instance of cacher. Built-in cachers: MemoryCacher or RedisCacher |
metrics | Boolean | false | Enable metrics function. |
metricsSendInterval | Number | 5000 | Metrics event sends period in milliseconds |
statistics | Boolean | false | Enable broker statistics. Measure the requests count & latencies |
validation | Boolean | false | Enable action parameters validation. |
internalActions | Boolean | true | Register internal actions for metrics & statistics functions |
heartbeatInterval | Number | 10 | Interval (seconds) of sending heartbeat |
heartbeatTimeout | Number | 30 | Timeout (seconds) of heartbeat |
ServiceFactory | Class | null | Custom Service class. Broker will use it when creating a service |
ContextFactory | Class | null | Custom Context class. Broker will use it when creating a context at call |
Call actions
You can call an action by calling the broker.call
method. Broker will search the service (and the node) that has the given action and it will call it. The function returns with a Promise
.
Syntax
let promise = broker.call(actionName, params, opts);
The actionName
is a dot-separated string. The first part of it is service name. The seconds part of it is action name. So if you have a posts
service which contains a create
action, you need to use posts.create
string as first parameter.
The params
is an object that will be passed to the action as part of the Context.
The opts
is an object. With this, you can set/override some request parameters, e.g.: timeout
, retryCount
.
Available options:
Name | Type | Default | Description |
---|
timeout | Number | requestTimeout of broker | Timeout of request in milliseconds. If the request is timed out and you don't define fallbackResponse , broker will throw a RequestTimeout error. Disable: 0 |
retryCount | Number | requestRetry of broker | Count of retry of request. If the request timed out, broker will try to call again. |
fallbackResponse | Any | null | Return with it, if the request is timed out. More info |
Usage
broker.call("user.list").then(res => console.log("User list: ", res));
broker.call("user.get", { id: 3 }).then(res => console.log("User: ", res));
broker.call("user.recommendation", { limit: 5 }, { timeout: 500, fallbackResponse: defaultRecommendation })
.then(res => console.log("Result: ", res));
broker.call("posts.update", { id: 2, title: "Modified post title" })
.then(res => console.log("Post updated!"))
.catch(err => console.error("Unable to update Post!", err));
Request timeout & fallback response
If you call action with timeout
and the request is timed out, broker throws a RequestTimeoutError
error.
But if you set fallbackResponse
in calling options, broker won't throw error, instead returns with this given value. It can be an Object
, Array
...etc.
This can be also a Function
, which returns a Promise
. In this case the broker will pass the current Context
to this function as an argument.
Emit events
Broker has an internal event bus. You can send events locally & globally. The local event will be received only by local services of broker. The global event that will be received by all services on all nodes.
Send event
You can send event with emit
and emitLocal
functions. First parameter is the name of event. Second parameter is the payload.
broker.emitLocal("service.started", { service: service, version: 1 });
broker.emit("user.created", user);
Subscribe to events
To subscribe for events use the on
or once
methods. Or in Service use the events
property.
In event names you can use wildcards too.
broker.on("user.created", user => console.log("User created:", user));
broker.on("user.*", user => console.log("User event:", user));
broker.on("**", payload => console.log("Event:", payload));
To unsubscribe call the off
method.
Middlewares
Broker supports middlewares. You can add your custom middleware, and it'll be called on every local request. The middleware is a Function
that returns a wrapped action handler.
Example middleware from validators modules:
return function validatorMiddleware(handler, action) {
if (_.isObject(action.params)) {
return ctx => {
this.validate(action.params, ctx.params);
return handler(ctx);
};
}
return handler;
}.bind(this);
The handler
is the request handler of action, what is defined in Service schema. The action
is the action object from Service schema. The middleware should return with the handler
or a new wrapped handler. In this example above, we check whether the action has a params
props. If yes we return a wrapped handler that calls the validator before calling the original handler
.
If there is no params
property we return the original handler
(skip wrapping).
If you don't call the original handler
it will break the request. You can use it in cachers. If you find the data in cache, don't call the handler, instead return the cached data.
Example code from cacher middleware:
return (handler, action) => {
return function cacherMiddleware(ctx) {
const cacheKey = this.getCacheKey(action.name, ctx.params, action.cache.keys);
const content = this.get(cacheKey);
if (content != null) {
ctx.cachedResult = true;
return Promise.resolve(content);
}
return handler(ctx).then(result => {
this.set(cacheKey, result);
return result;
});
}.bind(this);
};
Internal actions
The broker registers some internal actions to check the health of node or get request statistics.
List of local services
This action lists local services.
broker.call("$node.services").then(res => console.log(res));
List of local actions
This action lists local actions
broker.call("$node.actions").then(res => console.log(res));
List of nodes
This actions lists all connected nodes.
broker.call("$node.list").then(res => console.log(res));
Health of node
This action returns the health info of process & OS.
broker.call("$node.health").then(res => console.log(res));
Statistics
This action returns the request statistics if the statistics
is enabled in options.
broker.call("$node.stats").then(res => console.log(res));
Service
The Service is the other main module in the Moleculer. With the help of this you can define actions.
Schema
You need to create a schema to define a service. The schema has some main parts (name
, version
, settings
, actions
, methods
, events
).
Simple service schema
{
name: "math",
actions: {
add(ctx) {
return Number(ctx.params.a) + Number(ctx.params.b);
},
sub(ctx) {
return Number(ctx.params.a) - Number(ctx.params.b);
}
}
}
Base properties
The Service has some base properties in the schema.
{
name: "posts",
version: 1
}
The name
is a mandatory property so it must be defined. It's the first part of actionName when you call it with broker.call
.
The version
is an optional property. If you are running multiple version of the same service this needs to be set. It will be a prefix in the actionName.
{
name: "posts",
version: 2,
actions: {
find() {...}
}
}
You need to call the find
action as
broker.call("v2.posts.find");
Settings
You can add custom settings to your service under settings
property in schema. You can reach it in the service via this.settings
.
{
name: "mailer",
settings: {
transport: "mailgun"
},
action: {
send(ctx) {
if (this.settings.transport == "mailgun") {
...
}
}
}
}
Actions
The actions are the callable/public methods of the service. They can be called with broker.call
method.
The action could be a function (handler) or an object with some properties and with handler
.
The actions should be placed under actions
key in the service schema.
{
name: "math",
actions: {
add(ctx) {
return Number(ctx.params.a) + Number(ctx.params.b);
},
mult: {
cache: false,
params: {
a: "required|numeric",
b: "required|numeric"
},
handler(ctx) {
if (ctx.action.cache)
return Number(ctx.params.a) * Number(ctx.params.b);
}
}
}
}
You can call these actions as
broker.call("math.add", { a: 5, b: 7 }).then(res => console.log(res));
broker.call("math.mult", { a: 10, b: 31 }).then(res => console.log(res));
Inside the action you can sub-call other actions in other services with ctx.call
method. It is an alias to broker.call
, just set itself as parent context.
{
name: "posts",
actions: {
get(ctx) => {
let post = posts[ctx.params.id];
return ctx.call("users.get", { id: post.author }).then(user => {
if (user) {
post.author = user;
}
return post;
})
}
}
}
Events
You can subscribe to events and can define event handlers in the schema under events
key.
{
name: "users",
actions: {
...
},
events: {
"user.create": function(payload) {
this.logger.info("Create user...");
},
"user.*": function(payload, eventName) {
}
}
}
Methods
You can also create private functions in the Service. They are called as methods
. These functions are private, can't be called with broker.call
. But you can call it inside service actions.
{
name: "mailer",
actions: {
send(ctx) {
return this.sendMail(ctx.params.recipients, ctx.params.subject, ctx.params.body);
}
},
methods: {
sendMail(recipients, subject, body) {
return new Promise((resolve, reject) => {
...
});
}
}
}
The name of method can't be name
, version
, settings
, schema
, broker
, actions
, logger
, because these words are reserved.
Lifecycle events
There are some lifecycle service events, that will be triggered by ServiceBroker.
{
name: "www",
actions: {...},
events: {...},
methods: {...},
created() {
},
started() {
}
stopped() {
}
}
Properties of this
In service functions the this
is always binded to the instance of service. It has some properties & methods that you can use in service functions.
Name | Type | Description |
---|
this.name | String | Name of service from schema |
this.version | Number | Version of service from schema |
this.settings | Object | Settings of service from schema |
this.schema | Object | Schema definition of service |
this.broker | ServiceBroker | Instance of broker |
this.logger | Logger | Logger module |
this.actions | Object | Actions of service. Service can call its own actions directly. |
Create a service
There are several ways to create/load a service.
broker.createService()
Call the broker.createService
method with the schema of service as argument. You can use this method when developing or testing.
broker.createService({
name: "math",
actions: {
add(ctx) {
return Number(ctx.params.a) + Number(ctx.params.b);
},
sub(ctx) {
return Number(ctx.params.a) - Number(ctx.params.b);
}
}
});
Load service
You can place your service code to a single file and load this file with broker.
math.service.js
module.exports = {
name: "math",
actions: {
add(ctx) {
return Number(ctx.params.a) + Number(ctx.params.b);
},
sub(ctx) {
return Number(ctx.params.a) - Number(ctx.params.b);
}
}
}
index.js
let broker = new ServiceBroker();
broker.loadService("./math.service");
broker.start();
In the service file you can also be create the instance of Service. In this case you need to export a function that returns the instance of Service.
module.exports = function(broker) {
return new Service(broker, {
name: "math",
actions: {
add(ctx) {
return Number(ctx.params.a) + Number(ctx.params.b);
},
sub(ctx) {
return Number(ctx.params.a) - Number(ctx.params.b);
}
}
});
}
Or create a function that returns with the schema of service
module.exports = function() {
let users = [....];
return {
name: "math",
actions: {
create(ctx) {
users.push(ctx.params);
}
}
};
}
Load multiple services from a folder
You can load multiple services from a folder.
Syntax
broker.loadServices(folder = "./services", fileMask = "*.service.js");
Example
broker.loadServices();
broker.loadServices("./");
broker.loadServices("./svc", "user*.service.js");
Local/Private properties
If you would like to create private properties in service, we recommend to declare them in the created
handler.
Example for local properties
const http = require("http");
module.exports = {
name: "www",
settings: {
port: 3000
},
created() {
this.server = http.createServer(this.httpHandler);
},
started() {
this.server.listen(this.settings.port);
},
stopped() {
this.server.close();
},
methods() {
httpHandler(req, res) {
res.end("Hello Moleculer!");
}
}
}
Context
When you call an action, the broker creates a Context
instance which contains all request informations and pass to the action handler as argument.
Available properties & methods of Context
:
Name | Type | Description |
---|
ctx.id | String | Context ID |
ctx.requestID | String | Request ID. If you make sub-calls in a request, it will be the same ID |
ctx.parent | Context | Parent context, if it's a sub-call |
ctx.broker | ServiceBroker | Instance of broker |
ctx.action | Object | Instance of action |
ctx.params | Any | Params of request. Second argument of broker.call |
ctx.nodeID | String | Node ID |
ctx.logger | Logger | Logger module |
ctx.level | Number | Level of request |
ctx.call() | Function | You can make a sub-call. Same arguments like broker.call |
ctx.emit() | Function | Emit an event, like broker.emit |
Logging
In Services every modules have a custom logger instance. It is inherited from the broker logger instance and you can set in options of broker.
Every modules add a prefix to the log messages. Using that prefix you can identify the module.
let { ServiceBroker } = require("moleculer");
let broker = new ServiceBroker({
logger: console,
logLevel: "info"
});
broker.createService({
name: "posts",
actions: {
get(ctx) {
ctx.logger.info("Log message via Context logger");
}
},
created() {
this.logger.info("Log message via Service logger");
}
});
broker.call("posts.get").then(() => broker.logger.info("Log message via Broker logger"));
Console messages:
[BROKER] posts service registered!
[POSTS-SVC] Log message via Service logger
[CTX] Log message via Context logger
[BROKER] Log message via Broker logger
Try it on Runkit
Custom log levels
If you want to change log level you need to set logLevel
in broker options.
let broker = new ServiceBroker({
logger: console,
logLevel: "warn"
});
You can set custom log levels to every module by prefix.
let broker = new ServiceBroker({
logger: console,
logLevel: {
"*": "warn",
"BROKER": "info",
"CTX": "debug",
"CACHER": "warn",
"TX": "info",
"POSTS-SVC": "error"
"USERS-SVC": false
}
});
Cachers
Moleculer has built-in cache solutions. You have to do two things to enable it:
- Set a cacher instance to the broker in constructor options
- Set the
cache: true
in action definition.
let { ServiceBroker } = require("moleculer");
let MemoryCacher = require("moleculer").Cachers.Memory;
let broker = new ServiceBroker({
logger: console,
cacher: new MemoryCacher()
});
broker.createService({
name: "users",
actions: {
list: {
cache: true,
handler(ctx) {
this.logger.info("Handler called!");
return [
{ id: 1, name: "John" },
{ id: 2, name: "Jane" }
]
}
}
}
});
Promise.resolve()
.then(() => {
return broker.call("users.list").then(res => console.log("Users count: " + res.length));
})
.then(() => {
return broker.call("users.list").then(res => console.log("Users count from cache: " + res.length));
});
Console messages:
[BROKER] users service registered!
[USERS-SVC] Handler called!
Users count: 2
Users count from cache: 2
Try it on Runkit
Cache keys
The cacher creates keys by service name, action name, and hash of params of context.
The key syntax is
<actionName>:<parameters or hash of parameters>
So if you call the posts.list
action with params { limit: 5, offset: 20 }
, the cacher calculate a hash from the params. So next time if you call this action with the same params, it will find in the cache by key.
// Hashed cache key for "posts.find" action
posts.find:0d6bcb785d1ae84c8c20203401460341b84eb8b968cffcf919a9904bb1fbc29a
However the hash calculation is an expensive operation. But you can specify which parameters you want to use for caching. In this case you need to set an object for cache
property that contains the list of parameters.
{
name: "posts",
actions: {
list: {
cache: {
keys: ["limit", "offset"]
},
handler(ctx) {
return this.getList(ctx.params.limit, ctx.params.offset);
}
}
}
}
This solution is faster, so we recommend to use it in production environment.
Manual caching
You can also use the cacher manually. Just call the get
, set
, del
methods of broker.cacher
.
broker.cacher.set("mykey", { a: 5 });
let obj = broker.cacher.get("mykey", { a: 5 });
broker.cacher.del("mykey");
broker.cacher.clean();
Clear cache
When you create a new model in your service, sometimes you have to clear the old cache entries. For this purpose there are internal events. When an event like this is fired, the cacher will clean the cache.
{
name: "users",
actions: {
create(ctx) {
let user = new User(ctx.params);
ctx.emit("cache.clean");
ctx.emit("cache.clean", "users.*");
ctx.emit("cache.clean", [ "users.*", "posts.*" ]);
ctx.emit("cache.del", "users.list");
ctx.emit("cache.del", [ "users.model:5", "users.model:8" ]);
}
}
}
Memory cacher
MemoryCacher
is a built-in memory cache module.
let MemoryCacher = require("moleculer").Cachers.Memory;
let broker = new ServiceBroker({
cacher: new MemoryCacher({
ttl: 30
})
});
Redis cacher
RedisCacher
is a built-in Redis based cache module. It uses ioredis
client.
let RedisCacher = require("moleculer").Cachers.Redis;
let broker = new ServiceBroker({
cacher: new RedisCacher({
ttl: 30,
prefix: "SERVICER"
monitor: false
redis: {
host: "redis",
port: 6379,
password: "1234",
db: 0
}
})
});
Custom cacher
You can also create your custom cache module. We recommend you to copy the source of MemoryCacher
or RedisCacher
and implement the get
, set
, del
and clean
methods.
Transporters
Transporter is an important module if you are running services on more nodes. Transporter communicates with every nodes. Send events, call requests...etc.
NATS Transporter
Moleculer has a built-in transporter for NATS.
NATS Server is a simple, high performance open source messaging system for cloud native applications, IoT messaging, and microservices architectures.
let { ServiceBroker } = require("moleculer");
let NatsTransporter = require("moleculer").Transporters.NATS;
let broker = new ServiceBroker({
nodeID: "server-1",
transporter: new NatsTransporter(),
requestTimeout: 5 * 1000
});
Transporter options
You can pass options to nats.connect()
method.
new NatsTransporter();
new NatsTransporter("nats://nats.server:4222");
new NatsTransporter({
nats: {
url: "nats://nats-server:4222",
},
prefix: "MY-PREFIX"
});
new NatsTransporter({
nats: {
url: "nats://nats-server:4222",
user: "admin",
pass: "1234"
}
});
Redis Transporter
Moleculer has a built-in transporter for Redis.
let { ServiceBroker } = require("moleculer");
let RedisTransporter = require("moleculer").Transporters.Redis;
let broker = new ServiceBroker({
nodeID: "server-1",
transporter: new RedisTransporter(),
requestTimeout: 5 * 1000
});
Transporter options
You can pass options to new Redis()
method.
new RedisTransporter();
new RedisTransporter("redis://redis.server:4222");
new RedisTransporter({
redis: {
url: "redis://redis-server:4222",
},
prefix: "MY-PREFIX"
});
new RedisTransporter({
redis: {
url: "redis://redis-server:4222",
user: "admin",
pass: "1234"
}
});
MQTT Transporter
Moleculer has a built-in transporter for MQTT protocol (e.g.: Mosquitto).
let { ServiceBroker } = require("moleculer");
let MqttTransporter = require("moleculer").Transporters.MQTT;
let broker = new ServiceBroker({
nodeID: "server-1",
transporter: new MqttTransporter(),
requestTimeout: 5 * 1000
});
Transporter options
You can pass options to mqtt.connect()
method.
new MqttTransporter();
new MqttTransporter("mqtt://mqtt.server:4222");
new MqttTransporter({
mqtt: {
url: "mqtt://mqtt-server:4222",
},
prefix: "MY-PREFIX"
});
new MqttTransporter({
mqtt: {
url: "mqtt://mqtt-server:4222",
user: "admin",
pass: "1234"
}
});
Custom transporter
You can also create your custom transporter module. We recommend you that copy the source of NatsTransporter
and implement the connect
, disconnect
, subscribe
and publish
methods.
Metrics
Moleculer has a metrics function. You can turn on in broker options with metrics: true
property.
If enabled, the broker emits metrics events in every metricsSendInterval
.
Health info
Broker emits a global event as metrics.node.health
with health info of node.
Example health info:
{
"cpu": {
"load1": 0,
"load5": 0,
"load15": 0,
"cores": 4,
"utilization": 0
},
"mem": {
"free": 1217519616,
"total": 17161699328,
"percent": 7.094400109979598
},
"os": {
"uptime": 366733.2786046,
"type": "Windows_NT",
"release": "6.1.7601",
"hostname": "Developer-PC",
"arch": "x64",
"platform": "win32",
"user": {
"uid": -1,
"gid": -1,
"username": "Developer",
"homedir": "C:\\Users\\Developer",
"shell": null
}
},
"process": {
"pid": 13096,
"memory": {
"rss": 47173632,
"heapTotal": 31006720,
"heapUsed": 22112024
},
"uptime": 25.447
},
"net": {
"ip": [
"192.168.2.100",
"192.168.232.1",
"192.168.130.1",
"192.168.56.1",
"192.168.99.1"
]
},
"time": {
"now": 1487338958409,
"iso": "2017-02-17T13:42:38.409Z",
"utc": "Fri, 17 Feb 2017 13:42:38 GMT"
}
}
You can subscribe to it in your custom monitoring service.
Statistics
Moleculer has a statistics module that collects and aggregates the count & latency info of the requests.
You can enable it in broker options with statistics: true
property.
Broker emits global events as metrics.node.stats
. The payload contains the statistics. You need to enable metrics functions too!
Example statistics:
{
"requests": {
"total": {
"count": 45,
"errors": {},
"rps": {
"current": 0.7999854548099126,
"values": [
0,
6.59868026394721,
2.200440088017604
]
},
"latency": {
"mean": 0.8863636363636364,
"median": 0,
"90th": 1,
"95th": 5,
"99th": 12,
"99.5th": 12
}
},
"actions": {
"posts.find": {
"count": 4,
"errors": {},
"rps": {
"current": 0.599970001499925,
"values": [
1.7985611510791368,
0.20004000800160032
]
},
"latency": {
"mean": 7.5,
"median": 5,
"90th": 12,
"95th": 12,
"99th": 12,
"99.5th": 12
}
}
}
}
}
Nodes
Moleculer supports several architectures.
Monolith architecture
In this version you are running every services on one node with one broker. In this case every service can call other services locally. So there is no network latency and no transporter. The local call is the fastest.
Microservices architecture
This is the well-known microservices architecture when every service running on individual nodes and communicates others via transporter.
Mixed architecture
In this case we are running coherent services on the same node. It is combine the advantages of monolith and microservices architectures.
For example, if the posts
service calls a lot of times the users
service, we put them together, that we cut down the network latency between services. If this node is overloaded, we will add replicas.
Best practices (TODO)
- service files
- configuration
- benchmark
Benchmarks
Under development we are measuring every important parts of the framework that we can ensure the best performance.
Benchmark results
Test
$ npm test
or in development
$ npm run ci
Contribution
Please send pull requests improving the usage and fixing bugs, improving documentation and providing better examples, or providing some testing, because these things are important.
License
Moleculer is available under the MIT license.
Contact
Copyright (c) 2017 Ice-Services