pioc and hapi – some initial thoughts

hapi.js is an alternative to express and can be used to create a node.js server. Today I’m going to show you some of my initial thoughts on how pioc can be used together with hapi (current version: 8.0) to create node.js applications.

I’ve wanted to show an example of how to actually use pioc for quite some time now, however I never got the feeling that my express.js examples were good enough. With hapi, the situation is very different. hapi has been designed to be composable from the start and, as a result, it is a perfect match for a *dependency injection container* like pioc.

This is going to be a short introduction so we’ll only create a very basic application that simply prints a message and logs an event. I also assume that you are already familiar with hapi.js. If you’re not, please take a look at their website. There are some very good tutorials that’ll help you get started.

Now, let’s dive into our application architecture. We’ll start by taking a look at the directory structure:

- lib
  - pack.js
- modules
  - frontend
    - index.js
- index.js
- manifest.json

The lib directory will simply contain a simple glue function (pack) that’ll help us to create injectable hapi plugins (controllers).

This example is going to use only three modules which you can install using npm:

npm install --save hapi pioc es6-shim

The es6-shim package will make writing controllers a bit nicer.

index.js

// load the es6-shim
require('es6-shim');

// import Hapi, EventEmitter and pioc
var Hapi = require('hapi'),
    EventEmitter = require('events').EventEmitter,
    pioc = require('pioc'),
    // create our server module and load some services and controllers
    serverModule = pioc.createModule(__dirname)
        // our global "event bus"
        .value('vent', new EventEmitter())
        // a service that resolves to a message (which we'll print to the user)
        .bind('message', function() {
            // since we've loaded es6-shim, this will always be true
            if(Object.assign) {
                return 'Hello Universe (Object.assign exists)';
            }
            return 'Hello World';
        })
        // load the manifest service (contains plugin options)
        .value('manifest', require('./manifest'))
        // and finally load our frontend controller
        .load('modules/frontend'),
    // create an injector
    injector = pioc.createInjector(serverModule);
// create the Hapi Server
var server = new Hapi.Server();
// add a connection to port 3000 so that we can access our server as
// http://localhost:3000/
server.connection({ port: 3000 });

// for the startup, we'll want all services that start with "module"
// (there are better names for this but you get the idea)
// as well as our configuration manifest and - just to keep it simple - the event bus
injector.resolve(function(module, manifest, vent) {
    // register for an event
    vent.on('log:frontend:index', function(event) {
        // just log it to the console
        console.log('frontend index page visited');
    });
   
    // create an object that hapi will understand for every one of our application modules
    var modules = module.map(function(obj) {
        return {
            register: obj,
            // get the options for the module from the manifest
            options: manifest.plugins[obj.register.attributes.name] || {}
        };
    });
    // and register our modules with the server
    server.register(modules, function(err) {
        if(err) throw err;
       
        // finally, start the server
        server.start(function() {
            console.log('Server running at: ', server.info.uri);
        });
    });
});

There’s just a tiny bit of bootstrapping involved here and you can pretty much decide for yourself how much magic you want. For example, you could automatically load all modules from the modules directory or from the manifest.json file. The same holds true for other services. It is totally up to you if you want to do those things explicitly or if you want it to happen magically.

Now that we’ve got our bootstrap in place, let us take a look at what the manifest.json file could look like:

{
    "plugins": {
        "module/frontend": {}
    }
}

As you can see, there is almost nothing in here. Take a look at the hapi website to learn which options you might want to set for your application modules.

The really interesting part is the modules/frontend/index.js file, though. That is why we’re going through all that trouble and where we can actually benefit from using pioc, so let us take a look at it:

// import a few utilities
var inject = require('pioc').inject,
    pack = require('./../../lib/pack');

// This is our controller service!
// We can use the constructor injection provided by pioc
function Frontend(vent) {
    this.vent = vent;
}

// that is the reason why we've included es6-shim
Object.assign(Frontend.prototype, {
    // we can use property injection
    message: inject('message'),
   
    // define the action
    indexAction: function(request, reply) {
        // and use the event bus and message services we defined in our bootstrapping
        this.vent.emit('log:frontend:index', request);
        reply(this.message+'!');
    },
   
    // register our route handlers
    register: function(server, options, next) {
        server.route({
            method: 'GET',
            path: '/',
            handler: this.indexAction.bind(this)
        });
        next();
    }
});

// and package it all in a way that hapi and pioc are happy with
module.exports = pack(Frontend, {
   // this name is not only the name hapi is going to use but it'll also
   // be used by pioc to identify the name of the service!
    name: 'module/frontend',
    version: '1.0.0'
});

So, by doing things this way, we can define controller services which can rely on piocs ability to perform constructor and property injection. Also, the name of the service is where it should be: with the service itself instead of at the module definition. By using es6-shim, we get gain the ability to use Object.assign to get a much nicer syntax for defining our controller service.

But we’re still missing one crucial ingredient: the lib/pack.js file. The function exported there helps us to glue hapi and pioc together in a way that actually works.
You don’t really have to use it but it makes things a lot nicer and is not too magical.

// we need a constructor function (or just a function) as well as the attributes that Hapi needs
module.exports = function(cnstr, attributes) {
    // define our service, we'll need an $injector service which pioc supplies
    var service = function($injector) {
        // resolve the constructor (creates a new instance and resolves all services)
        var obj = $injector.resolve(cnstr),
            // and provide a Hapi plugin which registers the instance with Hapi
            plugin = {
                register: function(server, options, next) {
                    // we're passing the context as well as the default options
                    // in case it contains something useful
                    obj.register(server, options, next, this);
                }
            };
        // add the attributes to the register function
        plugin.register.attributes = attributes;
        return plugin;
    };
    // set the $serviceName flag so that pioc can use the correct service name
    service.$serviceName = attributes.name;
    return service;
};

As promised, there isn’t much magic involved in this function. We just wire hapi and pioc together and reuse as much information as we can.

When we run the server and check its output, we’ll see that the website correctly shows Hello Universe (Object.assign exists) and that the console has successfully logged the frontend index page visited event message.

So, those were some quick thoughts on how pioc can be used together with hapi.

Leave a Reply

Your email address will not be published. Required fields are marked *