This document discusses microservices using Node.js and JavaScript. It covers building an HTTP microservice with Express including routing, structure, database integration, logging and testing. It also discusses building command-based microservices with Seneca including patterns, plugins, and queueing. Finally, it discusses containerization with Docker, API gateways, testing, process management with PM2, and some considerations around when microservices may not be the best solution.
7. JavaScript is nearly everywhere.
There are a lot of OSS Modules.
Node.js was built for the web
Node.js supports most of the protocols, systems and
databases.
The platform is lightweight.
13. Express.js
Express is a lightweight (< 2MB) web application framework
for Node.js.
Express supports HTTP and HTTPS in version 1 and 2.
Express handles routing within a web application for you.
Additionally it provides you with a plugin interface
(Middleware).
21. Router
The router defines all available routes of a microservice.
A route consists of a HTTP method and a path. By adding
variables to routes they become more flexible.
To keep the code of your router readable, you should put the
routing callbacks in a separate file.
24. Controller
The controller holds your routing callback functions. It
extracts information from the requests and hands the
information over to the model.
To access variables of your routes, you can use the
req.params object.
To deal with information within the request body, you should
install the body-parser middleware. It introduces the req.body
object which represents the message body.
26. Model
The model contains the business logic of your application. It
also controls access to the database.
Usually every microservice has its own database, which
increases independence between services.
27. Model
Node.js supports all common databases such as OracleDB,
MySQL, Redis, MongoDB.
To access your database, you have to install a database driver
first.
yarn add sqlite3
To further simplify the access, you can use various ORMs or
ODMs
yarn add orm
28. ORM
An ORM handles CRUD operations for you. It already
implements all common use cases. You just have to
configure it with the structure of your database.
The ORM also takes care of some security features such as
escaping.
31. Model
Besides encapsulating database communication your model
takes care of additional tasks such as validation of user input
and various calculations.
Most of these operations are asynchronous. To provide a
clean API you should think about using promises, async
functions or streams instead of plain callbacks.
34. Logging
Within your microservice errors might occur at any time. Of
course you should be prepared to catch and handle them.
But you should also keep a log of errors somewhere.
A logger should not be a fixed part of a certain microservice.
logging is a shared service which is available for all services
in your application.
Centralised logging provides some advantages over local
logging such as scalability and an improved maintainability.
35. Logging
You can use log4js for remote logging in your application.
This library provides a plugin interface for external appenders
which support various log targets such as files or logstash.
For remote logging you could use the logstash appender and
a centralised logstash server.
39. Tests
Quality and stability are two very important aspects of
microservices. To ensure this, you need the ability to
automatically test your services.
There are two levels of tests for your microservice application:
unittests for smaller units of code and integration tests for
external interfaces.
For unittests you can use Jasmine as a framework. With
Frisby.js you can test your interfaces.
42. Unittests
To execute your tests, just issue the command npx jasmine.
You have two variants for organising your tests. You can either
store them in a separate directory, usually named “spec” or
you put your tests where your source code is located.
43. Mockery
In your microservice application you have to deal with a lot of
dependencies. They are resolved via the node module
system. Mockery is a tool that helps you with dealing with
dependencies. Mockery replaces libraries with test doubles.
yarn add -D mockery
45. Integration tests
Frisby.js is a library for testing REST interfaces. Frisby is an
extension for Jasmine. So you don’t have to learn an additonal
technology.
In order to run Frisby.js, you have to install jasmine-node.
yarn add -D frisby jasmine-node
46. Integration tests
require('jasmine-core');
var frisby = require('frisby');
frisby.create('Get all the articles')
.get('http://localhost:8080/article')
.expectStatus(200)
.expectHeaderContains('content-type', 'application/json')
.expectJSON('0', {
id: function (val) { expect(val).toBe(1);},
title: 'Mannesmann Schlosserhammer',
price: 7
})
.toss();
47.
48. PM2
Node.js is single threaded. Basically that’s not a problem
because of its nonblocking I/O nature. To optimally use all
available resources, you can use the child_process module.
A more convenient way of locally scaling your application is
using PM2.
yarn add pm2
51. Docker
All your services run in independent, self contained
containers.
You can start as many instances of a certain container as you
need.
To extend the features of your application, you simply add
additional services by starting containers.
55. API Gateway
Each service of your application has to focus on its purpose.
In order to accomplish this, you have to centralise certain
services. A typical example for a central service is
authentication.
An API Gateway forwards authorised requests to the services
of your application, receives the answers and forwards them to
the client.
56.
57. Seneca
Seneca follows a completely different approach. All services
of your application communicate via messages and become
independent of the transport layer.
59. Service Definition
const seneca = require('seneca')();
seneca.add({role: 'math', cmd: 'sum'}, controller.getAll);
The first argument of the add method is the pattern, which
describes the service. You are free to define the pattern as
you want. A common best practice is to define a role and a
command. The second argument is an action, a function that
handles incoming requests.
60. Service Handler
async getAll(msg, reply) {
try {
const articles = await model.getAll(req);
reply(null, JSON.stringify(articles));
} catch (e) {
reply(e);
}
}
Similar to express a service handler receives the
representation of a request. It also gets a reply function. To
create a response, you call the reply function with an error
object and the response body.
61. Service Call
seneca.act({role: 'article', cmd: 'get'}, (err, result) => {
if (err) {
return console.error(err);
}
console.log(result);
});
With seneca.act you consume a microservice. You supply the
object representation of a message and a callback function.
With the object, you trigger the service. As soon as the answer
of the service is available, the callback is executed.
63. Patterns
You can create multiple patterns of the same type. If a
service call matches multiple patterns, the most specific is
used.
You can use this behaviour to implement versioning of
interfaces.
65. Plugins
A plugin is a collection of patterns. There are multiple
sources of plugins: built-in, your own plugins and 3rd party
plugins.
Plugins are used for logging or debugging.
66. Plugins
You organise your patterns with the use method.
The name of the function is used for logging purposes.
You can pass options to your plugin to further configure it.
The init pattern is used instead of a constructor.
const seneca = require('seneca')();
function articles(options) {
this.add({role:'article',cmd:'get'}, controller.getAll);
}
seneca.use(articles);
67. Plugins
const seneca = require('seneca')();
function articles(options) {
this.add({role:'article',cmd:'get'}, controller.getAll);
this.wrap({role:'article'}, controller.verify);
}
seneca.use(articles);
The wrap method defines features that are used by multiple
patterns. With this.prior(msg, respond) the original service
can be called.
69. Server
function articles(options) {
this.add({role:'article',cmd:'get'}, controller.getAll);
this.wrap({role:'article'}, controller.verify);
}
require('seneca')()
.use(articles)
.listen(8080)
Just like in the ordinary web server the listen method binds
the server to the port 8080. Your service can then be called by
the browser or another server:
http://localhost:8080/act?role=article&cmd=get
71. Change the transport
// client
seneca.client({
type: 'tcp',
port: 8080
});
// server
seneca.listen({
type: 'tcp',
port: 8080
});
By providing the type ‘tcp’ the TCP protocol is used instead of
HTTP as a communication protocol.
74. Integration in Express
With this configuration seneca adds routes to your Express
application.
You have to adopt your seneca patterns a little bit. All routes
defined with seneca.act('role:web', {routes: routes}) are added
as Express routes.
Via the path-pattern a corresponding matching is done.
76. Queue
You can use various 3rd party plugins to communicate via a
message queue instead of sending messages over network.
The main advantage of using a Queue is decoupling client
and server. Your system becomes more robust.
79. Microservices, the silver
bullet?
Microservices might be a good solution for certain problems
but definitely not for all problems in web development.
Microservices introduce an increased complexity to your
application. There is a lot of Node.js packages dealing with
this problem. Some of these packages are outdated or of bad
quality.
Take a look at GitHub statistics and npmjs.com if it fits your
needs.