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.