In this post, you can get information about how things work together. If you would like to learn them from scratch please check their documentation.

Goal

Build a simple application with Seneca microservices.

There will be 3 services and they will run in their own docker containers.

  1. Web server: Responds to http requests. Consumes Greeter and Adder service.
  2. Greeter service: Responds with ‘Hello’ to name parameter.
  3. Adder service: Calculates sum of two numbers and returns it.

What is used?

Seneca, Docker, Node.js, Express, bluebird

In the end

Node.js-Express web server will receive http request with parameters name, number1 and number2. Http response will be prepared by using adder and greeter microservices.

Request: http://localhost:3000/?name=Alex&number1=7&number2=8

Response: Hello Alex, sum of numbers is 15

Code

Code can be checked via this repository: https://github.com/fatihAydinoglu/microservices

Docker compose

docker-compose is used for creating microservices architecture and manage them together. docker-compose.yml file :


version: '2'
services:
  server:
    build: ./server
    ports:
      - "3000:3000"
  greeter-service:
    build: ./greeter-service
  adder-service:
    build: ./adder-service

It is clear that we have 3 services. Only web server has port mapping to host. Please check also “Dockerfile” files in “build” folders in code repository.

“docker-compose up” command will build three services and start them.

Web server

We have only one route “/” which is handled by Express.


// server/app.js
const express = require('express');
const greeterService = require('./services/greeter');
const adderService = require('./services/adder');
const join = require("bluebird").join;

const app = express();

// query params: ?name=Alex&number1=10&number2=26
app.get('/', function (req, res) {
    join(
        greeterService.sayHello(req.query.name),
        adderService.sum(req.query.number1, req.query.number2),
        function (greeting, sum) {
            res.send(`${greeting.result}, sum of numbers is ${sum.result}`);
        }
    );
});

module.exports = app;

bluebird’s join is used for concurrent service calls. greeterService and adderService are called and http response is prepared with combining these two service responses.

To consume seneca services, there are two service client definitions in “/server/services” folder. In these files, seneca calls are promisified by bluebird and service methods are exported.


// server/services/adder.js
const seneca = require('seneca')();
const Promise = require('bluebird');

// Convert act to Promise
const act = Promise.promisify(seneca.client({ host: 'adder-service' }).act, { context: seneca });

// Service methods
const SUM = { role: 'adder', cmd: 'sum' };

// Call Service methods
const sum = (number1, number2) => {
    return act(Object.assign({}, SUM, { number1, number2 }));
};

module.exports = {
    sum
};

// server/services/greeter.js
const seneca = require('seneca')();
const Promise = require('bluebird');

// Convert act to Promise
const act = Promise.promisify(seneca.client({ host: 'greeter-service' }).act, { context: seneca });

// Service methods
const SAY_HELLO = { role: 'greeter', cmd: 'sayHello' };

// Call Service methods
const sayHello = (name) => {
    return act(Object.assign({}, SAY_HELLO, { name }));
};

module.exports = {
    sayHello
};

Greeter Service

This is a seneca service. In “index.js” file, “listen” function is called.


// greeter-service/index.js
require('seneca')()
  .use('greeter')
  .listen();

In “greeter.js” file service implementation is defined :


// greeter-service/greeter.js
module.exports = function (options) {
    this.add('role:greeter,cmd:sayHello', sayHello);

    function sayHello(msg, respond) {
        respond(null, { result: `Hello ${msg.name}` });
    }
}

Adder Service

This is another seneca service. It is similar to Greeter Service.


// adder-service/index.js
require('seneca')()
  .use('adder')
  .listen();

// adder-service/adder.js
module.exports = function (options) {
    this.add('role:adder,cmd:sum', sum);

    function sum(msg, respond) {
        respond(null, { result: Number(msg.number1) + Number(msg.number2)});
    }
}

Conclusion

With this simple implementation, you can have basic understanding of seneca microservices.

If you would like to check this code, clone from repository and run “docker-compose up”. You can browse “http://localhost:3000/?name=Mike&number1=10&number2=23” when services are started.

 

  • Bràñsøñ G Kürīå

    hey, is there a way to do this services without hardcoding ports? im imagining that its hard to do as we scale up to many services

    • Fatih Aydinoglu

      Hi Branson, you are right.

      There can be different cases for real world scenarios.

      Actually, in my simple demonstration, I don’t need port numbers to define. Only host parameter at seneca.client call would be enough. Default port can be used in every container, there is no need to have separate ports for each. I have removed ports from seneca.listen, also.

      Host names are still hardcoded, now. They can be also defined from shared configuration between services. Dockerfiles can define environment variables to be used. This depends on where and how these services will run.

      I have updated the code.
      Thanks for your comment.