Senecajs - A Microservice toolkit for Nodejs Ecosystem
The ever-expanding landscape of modern applications presents a constant challenge: balancing robust functionality with streamlined development and maintenance
If you find it insightful and appreciate my writing, consider following me for updates on future content. I'm committed to sharing my knowledge and contributing to the coding community. Join me in spreading the word and helping others to learn.
What is microservice?
If you are a newbie and fully aware of what microservice does, let me explain it in short. It will help you to grasp the general concept of Seneca that I'm about to articulate in detail.
Wikipedia says "In software engineering, a microservice architecture is a variant of the service-oriented architecture structural style. It is an architectural pattern that arranges an application as a collection of loosely coupled, fine-grained services, communicating through lightweight protocols." If I had to explain it to my 2-year-old daughter, I could have said this: Microservices are like small, special LEGO pieces in a big castle. Each piece does one job well, and you can change or fix one piece without messing up the whole castle. If you are keen to grasp the rudimentary concepts, I would suggest starting with this article.
Is Nodejs the best choice for Microservice?
Nodejs for microservice? Well, this is a bit controversial. You may experience a kaleidoscope of opinions - some shiny and positive, others a bit… murky. I'm a Nodejs developer so I have a natural inclination toward the positive side of it. But as a tech blogger, my duty should exhibit both sides of the argument. So, Let's begin!
Node.js has become a major player in the microservices game, and for good reason! Its event-loop-based architecture, featuring asynchronous and non-blocking I/O, allows developers to build applications that scale beautifully. In specific use cases, Node.js truly shines thanks to these core principles. Let's delve into some primary reasons why Node.js is the top choice for microservices architecture in some cases.
When Nodejs is not a good choice for Microservice
It doesn't mean Nodejs has turned out to be a knight in shining armor. There are some use cases too where Nodejs doesn't fit with the requirements.
Core Features of Seneca
If you have read this article so far, I assume you have already grasped the fundamentals of Microservice and the impact of Nodejs. I found this nice depiction while scoring through the internet of how Senecajs operates.
Now, let's back to our original topic to explore some core features of Senecajs.
Seneca Initialization
I think we have digested enough theory. Let's get our hands dirty with real implementations. Before we start implementing the microservice, we have to initialize Senecajs. However, there are multiple ways to do it. Let's take a look at a few approaches below.
Approach 1: Passing the configs explicitly - I would not recommend it though
const seneca = require('seneca')({
databaseURL: process.env.DB_URL,
// Other options...
});
Approach 2: Passing env variables - I consider this as a standard approach to define a common config.
const env = process.env.NODE_ENV || 'development';
const config = require(`./config/${env}.json`);
const seneca = require('seneca')(config);
Approach 3: Passing config in the command line - This is again not a good option for config management
node my-service.js --dbUrl=mydb.example.com
Approach 4: Using the Seneca plugin - This approach is also good for custom configs for different plugins
领英推荐
seneca.use('my-plugin', { option1: 'value1', option2: 'value2' });
Approach 5: Using Seneca Dynamic config - This is a useful approach too for setting up dynamic configuration at the runtime.
seneca.add({ role: 'config', cmd: 'set' }, (msg, reply) => {
// Update configuration
});
Step-by-step Implementation
Now, our Senecajs are initialized with project-specific custom config. Now, let's play with some working snippets.
1. Defining a Seneca plugin
// my-plugin.js
module.exports = myPlugin(options) => {
const seneca = this;
seneca.add({ role: 'math', cmd: 'sum' }, (msg, respond) => {
const { a, b } = msg;
const result = a + b;
respond(null, { result });
});
return 'my-plugin';
};
2. Creating an entry point for microservice
// microservice.js
const Seneca = require('seneca');
const seneca = Seneca();
seneca.use(require('./my-plugin'));
seneca.listen({ port: 3000, host: 'localhost' });
3. Start the microservice
> node microservice.js
4. Invoke microservice
// test-client.js
const Seneca = require('seneca');
const seneca = Seneca();
seneca.client({ port: 3000, host: 'localhost' });
seneca.act({ role: 'math', cmd: 'sum', a: 5, b: 3 }, (err, result) => {
if (err) {
console.error(err);
} else {
console.log('Sum:', result.result);
}
});
5. Create your REST API set to expose the result to the frontend
Logging with Seneca
Senecajs offers a comprehensive, well-structured mechanism for application logging. Let me write an example as follows:
const fs = require('fs')
function math(options) {
// the logging function, built by init
let log;
// place all the patterns together
// this make it easier to see them at a glance
this.add('role:math,cmd:sum', sum)
this.add('role:math,cmd:product', product)
// this is the special initialization pattern
this.add('init:math', init)
function init(msg, respond) {
// log to a custom file
fs.open(options.logfile, 'error.log', function (err, fd) {
// cannot open for writing, so fail
// this error is fatal to Seneca
if (err) return respond(err)
log = make_log(fd)
respond()
})
}
function sum({ left, right }, respond) {
const out = left + right;
log(`sum of ${left} + ${right} = ${out}`)
respond(null, out)
}
function product({ left, right }, respond) {
const out = left * right
log(`product of ${left} + ${right} = ${out}`)
respond(null, out)
}
function make_log(fd) {
return (entry) => {
fs.write(fd, `${new Date().toISOString()} ${entry}`, null, 'utf8', function (err) {
if (err) return console.log(err)
// ensure log entry is flushed
fs.fsync(fd, function (err) {
if (err) return console.log(err)
})
})
}
}
}
require('seneca')()
.use(math, {logfile:'./math.log'})
.act('role:math,cmd:sum,left:1,right:2', console.log)
Do you want to try it out on your own? Check out their official documentation.
Accessing microservices written in other languages from Seneca
After reading this far, you might be wondering, how Seneca would be able to connect microservices written in different languages. Senecajs utilizes communication protocols and interfacing techniques that enable seamless interaction between services written using multilingual programming.
Senecajs offers client libraries for various communication protocols, such as seneca-web for REST, and seneca-amqp for AMQP. Please note that Seneca has limited gRPC support. If you want to leverage the gRPC protocol, you have to make some tweaks. That's another topic of a detailed article.
Here's an example of how to connect with a REST microservice written in another language usingSenecaa-web
const Seneca = require('seneca');
const seneca = Seneca();
// Configure the client
seneca.use('seneca-web', {
url: 'https://<microservice-hostname>:<microservice-port>',
});
// Define an action to interact with the microservice
seneca.add('get-data', async (args, done) => {
try {
const response = await seneca.act('getData');
done(null, response.data);
} catch (error) {
done(error);
}
});
Challenges with Seneca
So far you have read through many good things about Seneca but this is not a silver bullet to every problem. It poses some concerning challenges too. The learning is incomplete if you do not understand the underlying challenges of a library or framework. So, let's explore them.
Please let me know what you feel about Senecajs. Do you find it exciting to dig down further on your own? The official Senecajs documentation might be helpful for you: https://senecajs.org/getting-started/ If you have already tried it out, please provide your feedback in the comment section, if not, let me know whether you find it interesting or not. Keep learning and keep sharing knowledge with others!