A microservices framework for Node.js
A microservice registers handlers under patterns (plain objects). Callers send a pattern and arguments; Devis routes the call to the matching handler, in the same process or over TCP, a Unix socket or a named pipe. Moving a service out of process does not change the calling code beyond adding clientId.
npm install devisNode.js 22 or later is required.
math.js registers two handlers and listens on port 3030:
const devis = require("devis");
devis.setName("math");
devis.push({ role: "math", cmd: "sum" }, (args, done) => {
done(null, args.left + args.right);
});
devis.push({ role: "math", cmd: "divide" }, (args, done) => {
if (args.right === 0) return done("division by zero");
done(null, args.left / args.right);
});
devis.listen({ host: "127.0.0.1", port: 3030 });client.js connects to it and calls both:
const devis = require("devis");
devis.setName("client").connect({ id: "math", host: "127.0.0.1", port: 3030 }, () => {
devis.call({ clientId: "math", role: "math", cmd: "sum" }, { left: 1, right: 2 }, (err, result) => {
console.log(result); // 3
devis.call({ clientId: "math", role: "math", cmd: "divide" }, { left: 1, right: 0 }, (err) => {
console.log(err); // division by zero
devis.close("math");
});
});
});node math.js
node client.js # in another terminalclientId is the id given to connect: it tells Devis which connection to send the call to.
- A call matches a handler only when its pattern is identical: same keys, same values, same key order.
{ role: "math" }does not match{ role: "math", cmd: "sum" }. - Registering the same pattern twice throws
This pattern already exists. - Calling a pattern nobody registered throws
No handle found. On a remote service, the callback receives"No handle found"aserrand the service keeps running. require("devis")returns one instance per process, so every file of a service shares the same patterns.
const devis = require("devis");
devis.push({ role: "greeter", cmd: "hello" }, (args, done) => {
done(null, "Hello " + args.name);
});
devis.call({ role: "greeter", cmd: "hello" }, { name: "Ada" }, (err, result) => {
console.log(result); // Hello Ada
});Export the instance from a module, then load its patterns with use (the path is relative to the calling file):
// services/users.js
const devis = require("devis");
devis.push({ role: "users", cmd: "get" }, (args, done) => {
done(null, { id: args.id, name: "Ada" });
});
module.exports = devis;// index.js
const devis = require("devis").use("./services/users");
devis.call({ role: "users", cmd: "get" }, { id: 1 }, (err, user) => {
console.log(user); // { id: 1, name: 'Ada' }
});listen and connect take the same options as Node.js net, so pass path instead of host and port:
// server
devis.push({ role: "ping" }, (args, done) => done(null, "pong"));
devis.listen({ path: "/tmp/devis-math.sock" }); // Windows: { path: "\\\\.\\pipe\\devis-math" }// client
devis.connect({ id: "local", path: "/tmp/devis-math.sock" }, () => {
devis.call({ clientId: "local", role: "ping" }, {}, (err, result) => {
console.log(result); // pong
});
});plug loads the patterns of an installed plugin:
| Plugin | Patterns |
|---|---|
| devis-mongo-client | { role: "mongodb", action: "connect" | "insert" | "find" | "update" | "delete" | ... } |
| devis-pub_sub-redis | { role: "pub" | "sub", action: "create" }, { transport: "pub/sub", action: "subscribe" | "pubslish" } |
| devis_rabbitmq | adds listenMQ, clientMQ and callMQ to call services through RabbitMQ |
npm install devis devis-mongo-clientconst devis = require("devis").plug("devis-mongo-client");
function call(action, args) {
return new Promise((resolve, reject) => {
devis.call({ role: "mongodb", action }, args, (err, res) => (err ? reject(err) : resolve(res)));
});
}
async function main() {
await call("connect", { url: "mongodb://localhost:27017/shop" });
await call("insert", { collection: "books", type: "many", data: [{ title: "Dune" }, { title: "Hyperion" }], close: false });
const books = await call("find", { collection: "books", query: {} });
console.log(books.map((book) => book.title)); // [ 'Dune', 'Hyperion' ]
}
main();More runnable examples, including Redis and RabbitMQ, are in Devis-Examples. To start a new project, use generator-devis.
| Method | Description |
|---|---|
push(pattern, handler) |
Registers handler(args, done) for pattern. Call done(err, result) to answer. |
call(pattern, [args], callback) |
Calls the handler matching pattern with args (default {}). Add clientId to the pattern to call a remote service. callback(err, result). |
listen(options, [callback]) |
Exposes the registered patterns. options are net options: { host, port } or { path }. |
connect(options, [callback]) |
Connects to a remote service. options.id is required and unique; it is the clientId used by call. |
close(id) |
Closes the connection id. |
use(path) |
Loads the patterns of a module exporting a Devis instance, relative to the calling file. |
plug(name) |
Loads the patterns of an installed plugin. |
setName(name) |
Names the instance; used in logs and in log(). |
log() |
Writes the local patterns to <name>-log.txt. |
Every method returns the instance, so calls can be chained.
- Node.js 22 or later is required.
- devisPattern 2 is plain JavaScript, so CMake and a C++ toolchain are no longer needed to install Devis.
- Each microservice now has its own pattern table. In 2.x, two matchers in the same process could resolve each other's patterns to the wrong handler.
- An error returned by a remote handler now reaches the caller's callback. In 2.x and 3.0.0 the client treated it as a missing pattern and threw
No handle found. - Calling an unknown pattern on a remote service now answers
"No handle found"to the caller. Up to 3.0.1 it crashed the remote service.
MIT