Node.js + microservices = Moleculer

If you have an enormous application that grows year by year, there will be a day when you realize that in your team there are only one, or maybe two people who understand code. So what shall you do with this knowledge? There are few possibilities:

  • Rewrite whole app like Microsoft is doing with Windows.
  • Refactor code – pay technological debt.
  • Do nothing just live with this (many companies are working like this way).

However, there is another choice…microservices. Imagine a system splitted into plenty of small apps communicating through one message broker. These small apps would be easier and faster to analyze. We, developers could focus only on logical part of application and write code instead of analyzing thousands of code lines.

Ok… We now know that microservices could be the resolution for our problems. But we have heard that this approach is difficult to design and even more difficult to implement. How can we do this? Well, we have many choices, but I would like to focus on using great framework called Moleculer.
It has its own CLI, allows to manage nodes and supports plenty of message brokers.

Shall we start?

This article will briefly explain how to create a project, configure a few services and what we can get for free from this framework. Let’s start from installing moleculer cli tool.

npm i moleculer-cli -g

Before making a new project, let’s talk a bit about the gateway. Having a gateway in the molecular project is not obligatory, but without it nobody will be able to access services from outside the message broker. I think that the goal of the project  is to give the user API and this is why we are adding a gateway. By default moleculer creates gateway with ApiGateway mixins, which creates api endpoint for each service. This gives us a lot of for free. When we have successfully installed the cli then we can create project.

moleculer init project tax-demo-api

Moleculer asks us few questions

For demo purposes main project will contain Gateway, use AMQP transporter (for RabbitMQ), communicate with other nodes, collect metrics and check tracing. At the end moleculer ask us if we want to automatically run npm install. Results?

After opening project in vscode we can see project structure. Most important files in the project are:

  • moleculer.config.js,
  • api.service.js,
  • greeter.service.js.

There is also index.html file, which shows moleculer console definition. File moleculer.config.js contains all available properties, which can be changed. Beginning from the`nodeId` ending on the load balancing, tracing, and metrics. We will leave all properties in defaul configuraiton beside 2 of them:

  • nodeId should not be null but master – this is unique name of the node,
  • transporter cannot be null if we want to use broker. In this case our broker will be RabbitMQ, so set tha property to amqp://localhost:5672.

Keep in mind that you need to have RabbitMQ started on your machine. I suggest to use docker image for RabbitMQ. For more info click here. Now it’s time to start an app by using command npm run dev or yarn run dev.

Moleculer informs us that he started an instance with 3 endpoints. Also there is a line in the terminal informing us about url address http://localhost:3000, where can find web console. After opening browser and typing provided url, moleculer shows console home page.

Browser displays instance configuration details, so we can see what was enabled, which transporter is used and what are it’s options setted. On the top of the site there is a menu containing 3 other tabs:

  • Greeter service – default page for accessing greeter service api. I would like to stay here for a while and explain how these tabs for services work. Unfortunatelly, if we add new service to the moleculer, it will automatically join with broker, show in nodes and services tab. But we will not see new tab for restful requests. These tabs are statically written in the index.html file. To add next service page, menu and requests properties in JSON object have to be modified. For demo purposes we will use curl utility to access endpoints.
  • Nodes – lists all available services. Displays nodeId, type, moleculer version, address, host name, and status (is it working or terminated). At the moment it shows only one node called master.
  • Services – here we can find all services available for public access. Right now there are two: api and greeter.

Shall we try one of them? Let’s run greeter.welcome

kamilb@kamil-pc ~/P/workspace_nodejs> curl --header "Content-Type: application/json"  \
--request POST \
--data '{"name":"John"}' \
http://localhost:3000/api/greeter/welcome

"Welcome, John"

Great, it works!!!

Defining service

Project is created and configured properly. Now let’s see Greeter service definition file.

"use strict";
 
/**
* @typedef {import('moleculer').Context} Context Moleculer's Context
*/
 
module.exports = {
 name: "greeter",
 
 settings: { },
 dependencies: [],
 hooks: { },
 
 actions: {
 
   hello: {
     rest: {
       method: "GET",
       path: "/hello"
     },
     async handler() {
       return "Hello Moleculer";
     }
   },
 
   welcome: {
     rest: "/welcome",
     params: {
       name: "string"
     },
     /** @param {Context} ctx  */
     async handler(ctx) {
       return `Welcome, ${ctx.params.name}`;
     }
   }
 },
 
 events: {},
 
 methods: {},
 
 created() { },
 
 async started() { },
 
 async stopped() { }
};

Let’s explain what this file contains:

  • name – should be unique service name,
  • dependencies – contains names of all services, it will wait till these services are not up and running,
  • settings – service settings, I will write more on that later,
  • hooks – we can define hook for actions, more on that here https://moleculer.services/docs/0.13/actions.html#Action-hooks,
  • actions – are public methods available in API to the end user,
  • events – here is definition of the events ,to which our service is listening,
  • methods – private methods available for this service – these methods can be used only in this service,
  • last three – created, started, and stopped are service’s lifecycle methods.

Ok. Now remove greeter.service.js file. What happened? There is no need to restart the application or refresh browser. Services page had automatically refreshed and now it shows only one endpoint given by GatewayAPI mixin.

New Tax service

Use cli to create new project. We need to reproduce all steps from creating api project besides one. We answer No to question Add API Gateway (moleculer-web) service?. Open project in the editor, set nodeId to first and transport to amqp://localhost:5672. Create tax.service.js file and implement calculate method inside it.


"use strict";
 
/**
* @typedef {import('moleculer').Context} Context Moleculer's Context
*/
module.exports = {
 name: "tax",
 
 settings: {},
 
 dependencies: [],
 
 actions: {
   welcome: {
     rest: "/calculate",
     params: {
       amount: "number"
     },
     async handler(ctx) {
       return ctx.params.amount * 0.23;
     }
   }
 },
 
 events: {},
 methods: {},
 created() {},
 async started() {},
 async stopped() {}
};

If on your computer there is already a running metrics instance from another node, don’t forget to change the port of the Prometeus. The following error is telling us that there is Prometeus started at port 3030. Change it in the moleculer config file to different port.


node:events:353
      throw er; // Unhandled 'error' event
      ^

Error: listen EADDRINUSE: address already in use :::3030
    at Server.setupListenHandle [as _listen2] (node:net:1290:16)
    at listenInCluster (node:net:1338:12)
    at Server.listen (node:net:1424:7)
    at PrometheusReporter.init (/home/kamilb/Projects/workspace_nodejs/tax/node_modules/moleculer/src/metrics/reporters/prometheus.js:70:15)
    at /home/kamilb/Projects/workspace_nodejs/tax/node_modules/moleculer/src/metrics/registry.js:72:15
    at Array.map ()
    at MetricRegistry.init (/home/kamilb/Projects/workspace_nodejs/tax/node_modules/moleculer/src/metrics/registry.js:70:42)
    at new ServiceBroker (/home/kamilb/Projects/workspace_nodejs/tax/node_modules/moleculer/src/service-broker.js:206:17)
    at MoleculerRunner.startBroker (/home/kamilb/Projects/workspace_nodejs/tax/node_modules/moleculer/src/runner.js:450:17)
    at /home/kamilb/Projects/workspace_nodejs/tax/node_modules/moleculer/src/runner.js:475:21
Emitted 'error' event on Server instance at:

Ok, ready, steady go. Type in the terminal yarn run dev. As expected, new node called first is available and services tab shows new service.

Awsome!!! We deployed newest tax service. I want to try it out, don’t you?

kamilb@kamil-pc ~/P/workspace_nodejs> curl --header "Content-Type: application/json"  --request POST --data '{"amount":12}' http://localhost:3000/api/tax/calculate

2.7600000000000002

It’s alive!!! Working like a charm.

Events…

Let’s say that our calculation has to be shared with any other service. How to handle this? Events are useful to achieve this goal. First create new project called mailing. It will do nothing beside console.log the provided parameters. New service will have implemented only events part.


 events: {
   "mail": {
     group: "other",
     handler(ctx) {
         console.log("Payload:", ctx.params);
     }
   }
 },

After starting project, another node appears on the console. But how our tax service can communicate with mailing? We cannot call action, because it does not have any. Now we will use a broker to emit event in the calculated action from the tax service. To do this just add one simple line. First attribute of emit method invocation is event name (the same like in mailing service) and second attribute are params, which will be send to the recipient.

 async handler(ctx) {
       const tax = ctx.params.amount * 0.23;
       this.broker.emit("mail", {amount: ctx.params.amount, tax: tax});
       return tax
     }

And here is the result visible in the console of the mailing service

mol $ Payload: { amount: 12, tax: 2.7600000000000002 }

Pretty simple, don’t you think? Probably you are thinking: Ok, but why use events? Perhaps we could implement action in mailing and call it. Yes, we can use

ctx.broker.call("service.actionName", ...params);

But there is a small difference between events and directly calling actions. During event emitting there is a possibility to decide if this event should be propagated to all of the nodes/services or maybe just one. By using broker.call we can only call one action at the time.

Versioning

Today our boss came back with a great idea of changing the tax.calculate method by adding parameters, which will tell what is the rounding strategy (up/down). But many customers are using existing action in their environments. We want to implement a new version, but we do not want to break our customers systems.

What shall we do?

Nothing easier. Create project tax2, implement calculate method and add version property just after the name of the service to tell moleculer that there are 2 instances of one service.

 module.exports = {
 name: "tax",
 version: 2,

The same property has to be set in old version of the tax service (value for the version property should be 1)

Well done!!! Now in the services tab there are more rows. But wait, 2 of them are almost the same. Only difference between tax instances is prefix. All versioned services have v{version_number} prefix what helps with identification of the correct one.

I cannot wait to see the results

kamilb@kamil-pc ~/P/workspace_nodejs> curl --header "Content-Type: application/json" \
                                       --request POST \
                                       --data '{"amount":12, "round": true}' \
                                       http://localhost:3000/api/v1/tax/calculate
2.7600000000000002
kamilb@kamil-pc ~/P/workspace_nodejs> curl --header "Content-Type: application/json" \ --request POST \ --data '{"amount":12, "round": true}' \ http://localhost:3000/api/v2/tax/calculate 3

Summary

In this article we have seen, how fast we can create microservices architecture in node.js by using Moleculer. I explained the small percent of functionalities, which are available in this awesome project. There are out of the box features, like: authentication, db mixin, which creates default actions  eg. put/post/get/delete for entity scheme, there is also validation and more. If I would have to explain everything, it could take months. What I wanted to was to interest you, dear reader, to go deeper and check if this framework is worth your attention.

In my opinion it is.

Kamil Burek
About

Opensource enthusiast. Software engineer who loves Java and any other JVM language. Mainly backend developer, passionate about simple solutions to difficult problems.