Learn about the Seneca framework in this guest post by Diogo Resende, a microservices expert with over 15 years of development experience.

The Seneca framework has been designed to help develop message-based microservices. It has two distinct characteristics:

  • Transport agnostic: Communication and message transport is separated from your service logic, and it’s easy to swap transports
  • Pattern matching: Messages are JSON objects, and each function exposes the sort of messages it can handle, based on the object properties

Being able to change transports is not a big deal; many tools allow you to do so. What is really interesting about this framework is its ability to expose functions based on object patterns. 

Start by installing Seneca:

npm install seneca

For now, forget the transport and create a producer and consumer in the same file. Here’s an example:

const seneca  = require("seneca");
const service = seneca();

service.add({ math: "sum" }, (msg, next) => {
    next(null, {
        sum : msg.values.reduce((total, value) => (total + value), 0)
    });
});

service.act({ math: "sum", values: [ 1, 2, 3 ] }, (err, msg) => {
    if (err) return console.error(err);

    console.log("sum = %s", msg.sum);
});

There’s a lot to absorb. The easy part comes first as you include the seneca module and create a new service.

Then a producer function that matches an object with math equal to sum is exposed. This means that any request object to the service that has the property math and that is equal to sum will be passed to this function.

This function accepts two arguments. The first one, msg, is the request object (the one with the math property and anything else the object might have). The second argument, next, is the callback that the function should invoke when finished or in the case of an error. In this particular case, you’re expecting an object that also has a values list, and you’re returning the sum of all the values by using the reduce method available in arrays.

Finally, act is invoked to consume the producer. An object with math equal to sum and a list of values is passed. The producer should be invoked and should return the sum.

Assuming you have this code in app.js, if you run it in the command line, you should see something like this:

$ node app

sum = 6

It’s time to try and replicate the previous stack example. This time, instead of having the consumer and producer in the code, try using curl as the consumer.

For this, you first need to create service by loading Seneca and creating an instance:

const seneca = require("seneca");
const service = seneca({ log: "silent" });

You have to tell it explicitly that you don’t care about logging for now. Now, create a variable to hold the stack:

const stack = [];

Then, move on to creating the producers. For the purposes of this tutorial, create three producers: push to add an element to the stack, pop to remove the last element from the stack, and get to see the stack. Both push and pop will return the final stack result. The third producer is just a helper function for you to see the stack without performing any additional operations.

To add elements to the stack, define the following:

service.add("stack:push,value:*", (msg, next) => {
    stack.push(msg.value);

    next(null, stack);
});

There are a few new things to see here:

  • You’ve defined your pattern as a string instead of an object. This action string is a shortcut to the extended object definition.
  • You’ve explicitly indicated that you need a value.
  • You’ve also indicated that you don’t care what the value is.

Now define a simpler function to remove the last element of stack:

service.add("stack:pop", (msg, next) => {
    stack.pop();

    next(null, stack);
});

This one is simpler as you don’t need a value; you’re just removing the last one. You’re not addressing a case where the stack is empty already. An empty array won’t throw an exception, but in a real scenario, you may want another response.

The third function is even simpler as you’re just returning the stack:

service.add("stack:get", (msg, next) => {
    next(null, stack);
});

Finally, you need to tell service to listen for messages. The default transport is HTTP and you just have to indicate port 3000:

service.listen(3000);

Wrap all this code in a file and try it out. You can use curl or just try it in your browser. Seneca won’t differentiate between HTTP verbs in this case. Start by checking the stack. The URL describes the action (/act) that you want to perform, and the query parameter gets converted to the required pattern:

Seneca Framework

You can then add one to your stack and see the final stack:

Seneca Framework

Continue adding values (for example, two) and see how the stack grows:

Seneca Framework

If you then try removeing the last element, you’ll see the stack shrinking:

Seneca comes with middleware that you can install and use. In this case, the middleware are called plugins. By default, Seneca includes a number of core plugins for transport, and both HTTP and TCP transports are supported. There are more transports available, such as Advanced Message Queuing Protocol (AMQP) and Redis.

Moreover, storage plugins for persistent data are available, with support for several database servers—both relational and non-relational. Seneca exposes an object-relational mapping (ORM)-like interface to manage data entities. You can manipulate entities, use a simple storage in development, and then move to production storage later on. Here’s a more complex example:

const async   = require("async");
const seneca  = require("seneca");
const service = seneca();

service.use("basic");
service.use("entity");
service.use("jsonfile-store", { folder : "data" });

const stack = service.make$("stack");

stack.load$((err) => {
    if (err) throw err;

    service.add("stack:push,value:*", (msg, next) => {
        stack.make$().save$({ value: msg.value }, (err) => {
            return next(err, { value: msg.value });
        });
    });

    service.add("stack:pop,value:*", (msg, next) => {
        stack.list$({ value: msg.value }, (err, items) => {
            async.each(items, (item, next) => {
                item.remove$(next);
            }, (err) => {
                if (err) return next(err);

                return next(err, { remove: items.length });
            });
        });
    });

    service.add("stack:get", (msg, next) => {
        stack.list$((err, items) => {
            if (err) return next(err);

            return next(null, items.map((item) => (item.value)));
        });
    });

    service.listen(3000);
});

Just run this new code and see how this code behaves by making some requests to test it. First, see how the stack is:

Seneca Framework

Nothing different, right? Now, add the one to the stack:

Seneca Framework

Well, you haven’t received the final stack. You could have but, instead, you changed the service to return the exact item that was added, just for the sake of confirmation. Now add another value:

Seneca Framework

Again, it returns the value you just added. Here’s the stack:

Seneca Framework

Your stack now has two values. Now comes one big difference compared with the previous code. You’re using entities, an API exposed by Seneca, which helps you store and manipulate data objects using a simple abstraction layer similar to an ORM, or to people who are familiar with Ruby, an ActiveRecord.

The new code, instead of just popping out the last value, removes a value you indicate. So, remove the value one instead of two:

Success! You removed exactly one item. The code will remove all items from the stack that match the value (it has no duplication check, so you can have repeated items). Try to remove the same item again:

Seneca Framework

No more items match one, so it didn’t remove anything. Now check whether the stack still has two:

Seneca Framework

Of course, it does! You can try stopping and restarting the code; you’ll see that the stack will still have the value two. This is because you’re using the JSON file store plugin. Please note that when you’re testing using Chrome or any other browser, be aware of the requests made by the browser in advance while you’re typing. Because you’ve already tested the first code, which had the same URL addresses, the browser might duplicate requests and you might get a stack with duplicated values.