Jake Pruitt

This is where I write.

← Home

Plugin

Many of the most popular Node.js frameworks provide a basic structure for applications, and allow the user to create plugins or middleware that extends the basic functionality of the framework. Every framework has a variety of plugins, both official and unofficial, that create an ecosystem of users that all cooperate to not duplicate common tasks. These frameworks work more like a platform than a single product: they are the medium of communication for a variety of developers, as well as a glue that composes all of the disparate plugins into useful applications.

Express - Concatenation

A good plugin system is characterized by clear contracts between the framework and the plugin. In Express, the middleware system is very well understood and has clear expectations about what a plugin should provide. For example, to create a plugin for Express to use, all you need to create is a function with a specific signature, and pass it into app.use() or any app.VERB()

var app = express();

// a middleware with no mount path; gets executed for every request to the app
app.use(function (req, res, next) {
  console.log('Time:', Date.now());
  next();
});

As long as next() is called, the request will continue through all of the other middleware in the app. It is very easy to integrate a third-party middleware, such as cookie-parser:

var express = require('express');
var app = express();
var cookieParser = require('cookie-parser');

app.use(cookieParser());

The author of cookie-parser knew that his or her module only needed to return a function with the signature function(req, res, next) and call next() at the end of the function. Express is very powerful due to is strict adherence to this API: Everything is middleware in Express, from route handlers to application-wide authentication. The power of JavaScript I/O really shines in Express’s API.

Hapi - Registration

Hapi has a bit more formal approach to plugins, where each plugin is an object that needs a register function and attributes. Like Express, the register function has a clearly defined signature:

var myPlugin = {
  register: function(server, options, next) {
    next();
  }
};

myPlugin.register.attributes = {
  name: 'myPlugin',
  version: '1.0.0'
};

This structure makes it easy for an application to be split up into separate modules, where each module defines a plugin. That way, the entire application can be defined as a set of plugins acting on the server rather than one monolithic server. If the plugins are split up into separate modules, they can easily be installed and included via Hapi’s registration methods:

var Hapi = require('hapi');

var server = new Hapi.Server();
server.connection({ port: 3000 });

// load multiple plugins
server.register([{register: require('myplugin')},{register: require('yourplugin')}], function (err) {
  if (err) {
    console.error('Failed to load a plugin:', err);
  }
});

server.start(function () {
  console.log('Server running at:', server.info.uri);
});

There are also methods for registering plugins to specific connections of a server, which allows for even more modularity.

Seneca - Using and naming

Right now I am using Seneca.js for a checklist project. One of the beauties of Seneca is its flexibility, as can be seen on the homepage of their site. Application logic with Seneca can be created on a white board, and can be structured in almost any way imaginable. However, this flexibility has caused a bit of a difficulty for me in creating a new plugin, since example plugins and built-in plugins don’t always have easily understandable API’s, and it’s difficult to find common conventions. For instance, in creating a quick JSON HTTP API plugin, the following example is given:

module.exports = function( options ) {
  // you need a middleware function to look for a matching URL
  this.act('role:web',{use:function(req,res,next) {
    // look for a URL
    if( 0 == req.url.indexOf('/quick') ) {
      res.writeHead(200,{
        'Content-Type': 'application/json'
      })
      res.end('{"data":"quick response!"}')
    }
    else next()
  }})
  // register this plugin with seneca
  return { name:'quick' }
}

The plugin definition is done by performing actions on the this seneca instance. The entire seneca framework can be reduced to the idea of adding actions and calling them, and plugins provide a way to extract actions into separate modules and reuse them in a number of applications. The seneca-web plugin is being utilized in the above code in the role:web action. There are many plugins available, such as seneca-account and seneca-mail. These plugins can be registered with a seneca instance in the following way:

seneca.use('user',{confirm:true})
seneca.use('mail')
seneca.use('auth')
seneca.use('account')
seneca.use('project')
seneca.use('settings')
seneca.use('data-editor')

Seneca implicitly looks up these plugins within its built-in registry and scans the node_modules folder for modules with the seneca- prefix. This implicit model is a bit different from the very explicit plugin system of Express and Hapi. I will be creating a Seneca.js plugin soon for Hapi, and will become very familiar with plugin infrastructure. It’s difficult to reason around two simultaneous plugin systems, but I think it can be done. The trick will be to do it with a simple API with clearly defined expectations.