The Pursuit of Hapi-ness

An introduction to Hapi 6.0

Fionn Kelleher
18 min readMar 16, 2014

UPDATE: This guide is extremely outdated!

I’m currently writing a book on hapi for O’Reilly called Getting Started with hapi.js. The book covers all of hapi’s main aspects, and promotes testing throughout using lab. I’ll be keeping it up to date with new versions of hapi, so if you’re looking to master the framework, this book could be for you. It’ll be available on O’Reilly’s website as well as Amazon in the near future.

hapi core developers have also written Developing a hapi Edge — if you’re looking to get started now, it’s worth taking a look at too!

“The Pursuit of Hapi-ness: An Introduction to Hapi 3.0” — Winner of the Getting Started Tutorial Contest for Hapi!

Hapi is a configuration-centric HTTP framework developed by Walmart Labs for Node.js, powering their backend mobile platform. It’s gained a lot of traction in the community recently with more and more companies starting to make use of it, such as PayPal, Beats Audio and Mozilla.

In this tutorial, we’ll cover the essentials of the following topics:

  • Initialising a Hapi application
  • Basic routing
  • Data validation
  • Server method caching
  • Views and static assets
  • Server and request-level logging
  • The plugin architecture
  • Managing multiple servers with packs
  • Composing servers entirely from a configuration object
  • Launching servers from a configuration using the Hapi executable
  • Testing services using Lab (a separate tutorial)

Initialising The Application

The first thing we’ll do is initialise our new Hapi application, entitled “MyApp” in this case.

bash-3.2$ mkdir MyApp
bash-3.2$ cd MyApp/
bash-3.2$ npm init
name: (MyApp)
version: (0.0.0) 0.0.1
description: An example Hapi application
entry point: (index.js)
test command: mocha
git repository:
keywords:
author: Fionn Kelleher <me@fionn.co>
license: (ISC) BSD
About to write to /Users/fionnkelleher/MyApp/package.json:
{
"name": "MyApp",
"version": "0.0.1",
"description": "An example Hapi application",
"main": "index.js",
"scripts": {
"test": "mocha"
},
"author": "Fionn Kelleher <me@fionn.co>",
"license": "BSD"
}

Is this ok? (yes)

npm init generates a package.json file in the current directory based on inputted options.

With this completed, we can install Hapi as a dependency of MyApp.

bash-3.2$ npm install hapi@6.x —-save
npm WARN package.json MyApp@0.0.1 No repository field.
npm WARN package.json MyApp@0.0.1 No README data
npm http GET https://registry.npmjs.org/hapi
npm http 200 https://registry.npmjs.org/hapi
npm http GET https://registry.npmjs.org/hapi/-/hapi-6.0.0.tgz
npm http 200 https://registry.npmjs.org/hapi/-/hapi-6.0.0.tgz
[...]
hapi@6.0.0 node_modules/hapi
bash-3.2$

Hapi will be installed in the node_modules directory, as well as being added to the dependencies field of the package.json file we generated earlier, allowing for easy installation of our packaged application in production via a simple npm install.

Creating Our Server

To kick things off, we need to create a Hapi server and bind it to a host and port. Create an index.js with the following:

var Hapi = require("hapi");var server = new Hapi.Server(8080, "localhost");server.start(function() {
console.log("Hapi server started @", server.info.uri);
});

First, we require the main Hapi module. We create a new Hapi.Server object set to listen on port 8080 on “localhost”, and assign it to the server variable.

The Hapi.Server object contains many useful methods to configure the server, which we’ll delve into shortly. One of these is the “start” method, which makes the server listen on the port and host specified when creating it. It accepts a callback that gets executed when the server has started to listen for requests.

Execute index.js via node, and we’ll be greeted with Hapi server started @ http://localhost:8080 if all went well. Using our web browser—or a command line utility such as cURL—send a request to the Hapi server. Straight away, we’re greeted with the following JSON object.

{"statusCode":404,"error":"Not Found"}

Since we haven’t added any routes yet, there are no resources that can be accessed through the server.

Routing

Routing allows the server to react differently based on the HTTP path requested and method used. Hapi exposes routing at the highest level, without tangled, complex regular expressions.

To add a route to the server’s routing table, we use the route method, which requires an object containing the path, method and handler keys at a minimum. Here’s a quick usage example; add this to index.js before server.start() is called.

server.route({
path: "/",
method: "GET",
handler: function(request, reply) {
reply("Hello, world!");
}
});

path

Hapi allows us to define routes to match HTTP paths that are in compliance with the RFC 3986 specification. In addition, Hapi’s router provides parameterized paths, allowing us to extract segments of a path.

Here are some examples of valid values for the path key in the route object.

method

The method key of the route object defines what HTTP method that route deals with, be it GET, POST, PUT, DELETE or PATCH. The method can also be specified as an asterisk (*), signifying that the route will match any method.

handler

The handler key is an object or function that specifies the action to be taken when a request matches the route. More often than not, the handler will be a function with the signature function(request, reply) {…}.

The first parameter passed to the handler is a Hapi request object. To quote Hapi’s reference on the request object:

The request object is created internally for each incoming request. It is not the node request object received from the HTTP server callback (which is available in request.raw.req). The request object methods and properties change through the request lifecycle.

The request object allows us to access path parameters and query string components amongst other details.

The reply interface allows us to send a reply to the client. When called as a function, reply can take any of the following:

  • A String.
  • A JavaScript Object/Array (which will be JSON.stringify’d).
  • A Buffer.
  • A ReadableStream.

Putting It All Together

Add a route definition as follows before our server is started.

server.route({
path: "/hello/{name*2}",
method: "GET",
handler: function(request, reply) {
var names = request.params.name.split("/");
reply({
first: names[0],
last: names[1],
mood: request.query.mood || "neutral"
});
}
});

Send a request to http://localhost:8080/hello/John/Doe and we’ll be returned the following JSON object.

{"first":"John","last":"Doe":"mood":"neutral"}

Send a request to http://localhost:8080/hello/Jinny/Doe?mood=happy, and we’ll be returned the following JSON object.

{"first":"Jinny","last":"Doe","mood":"happy"}

Validation

It’s often desirable to validate data passed to the route, especially so whilst building an API. Hapi makes this a breeze by using the Joi object schema validation module, also developed by Spumko.

First, let’s use npm to install the Joi module and save it to our project’s dependencies.

bash-3.2$ npm install joi@4.x --save
npm WARN package.json MyApp@0.0.1 No repository field.
npm WARN package.json MyApp@0.0.1 No README data
npm http GET https://registry.npmjs.org/joi
npm http 304 https://registry.npmjs.org/joi
npm http GET https://registry.npmjs.org/hoek
npm http 304 https://registry.npmjs.org/hoek
joi@4.6.0 node_modules/joi
└── hoek@2.2.0

To begin using Joi validation, we need to require the module. Add the following line after we require Hapi.

var Joi = require("joi");

We use the config key in the route object to express validation rules. The config key is an object that allows us to split the route information from its implementation, and allows the definition of validation rules, cache settings and more.

Let’s define our route configuration.

var helloConfig = {
handler: function(request, reply) {
var names = request.params.name.split("/");
reply({
first: names[0],
last: names[1],
mood: request.query.mood
});
},
validate: {
params: {
name: Joi.string().min(8).max(100)
},
query: {
mood: Joi.string().valid(["neutral","happy","sad"]).default("neutral")
}
}
};

The object now contains the handler — which can either be a part of the config object or the route object — and Joi schemas.

The validate key can be used to define schemas matching against:

  • query — query string components
  • payload — the request body
  • params — parameterized path segments

In this case, our configuration says the following:

  • The name segment of the path must be at least eight characters long, and at maximum, 100 characters long.
  • The mood query string component is only valid if it’s value is either “neutral”, “happy” or “sad”. If the component is omitted from the request, Joi will set it to “neutral”, freeing us from doing this in our handler.

Our modified route is as follows:

server.route({
path: "/hello/{name*2}",
method: "GET",
config: helloConfig
});

Restart the Hapi server and make a request to http://localhost:8080/hello/J/Doe and we will be presented with the following object, containing a human-readable explanation as to why the request is not valid.

{"statusCode":400,"error":"Bad Request","message":"the length of name must be at least 8 characters long","validation":{"source":"path","keys":["name"]}}

If we request http://localhost:8080/hello/John/Doe we will get the same JSON object as we got before.

{"first":"John","last":"Doe":"mood":"neutral"}

Let’s set the mood to “excited”, and see what response we get. Make a request to http://localhost:8080/hello/John/Doe?mood=excited.

{"statusCode":400,"error":"Bad Request","message":"the value of mood must be one of neutral, happy, sad","validation":{"source":"query","keys":["mood"]}}

Again, Hapi sends us a concise error message specifying both a human-readable error message and the specific area that Joi reported wasn’t valid.

Let’s go one step further and add a query parameter for the person’s age. Modify the route’s configuration to the following (we’re adding the age key to the response, and a validation rule for Joi).

var helloConfig = {
handler: function(request, reply) {
var names = request.params.name.split("/");
reply({
first: names[0],
last: names[1],
mood: request.query.mood,
age: request.query.age
});
},
validate: {
params: {
name: Joi.string().min(8).max(20)
},
query: {
mood: Joi.string().valid(["neutral","happy","sad"]).default("neutral"),
age: Joi.number().integer().min(13).max(100)
}
}
};

When we request http://localhost:8080/hello/John/Doe?age=19 we’ll be sent the following JSON object:

{"first":"John","last":"Doe","mood":"neutral","age":19}

Notice that “age” gets converted to a Number automatically by Joi before being assigned to request.query. If we attempt to specify age as something other than an integer, such as a string or floating point number, the validation won’t pass.

For example, if we request http://localhost:8080/hello/John/Doe?age=no we’ll get a Joi error.

{"statusCode":400,"error":"Bad Request","message":"the value of age must be a number","validation":{"source":"query","keys":["age"]}}

Joi provides a lot of neat validation functions, which you can find out about on its GitHub repository.

Caching

Caching is also straightforward. Hapi utilises yet another module of Spumko’s, catbox, to abstract cache backends. At the time of writing, catbox caching supports a limited in-memory cache (used by default), Redis, MongoDB, Memcached and Riak.

To set up caching with our Hapi server, we’ll pass a server configuration object to the constructor containing our catbox configuration. In this tutorial, we’ll use a simple Redis cache, but you can find information on catbox engines over here.

Install catbox-redis and add it to our dependencies via npm. The creation of our Server object now looks like the following:

var server = new Hapi.Server(8080, "localhost", {
cache: {
engine: require("catbox-redis"),
options: {
host: "localhost",
partition: "MyApp",
password: "mypassword"
}
}
}
);

Server Methods

Server methods allow us to assign a commonly used utility function to the Server instance.

Let’s create a method to return a random colour from an array.

server.method("getColour", function(next) {
var colours = ["red", "blue", "indigo", "violet", "green"];
var colour = colours[Math.floor(Math.random() * colours.length)];
next(null, colour);
});

We can use this in our route handler as follows.

function(request, reply) {
var names = request.params.name.split("/");
server.methods.getColour(function(err, colour) {
reply({
first: names[0],
last: names[1],
mood: request.query.mood,
age: request.query.age,
colour: colour
});
});
}

Each time we make a valid request, the colour is randomly selected from the array and sent back to the client. But what if we wanted to generate the colour based on the name, and cache it for future requests?

As with most parts of Hapi, we can pass a configuration object to server.method(). Using the method configuration object, we can easily set up caching rules.

Let’s modify the method to also take a name as a parameter, and an options object specifying how long the colour matching the name should be cached for.

server.method("getColour", function(name, next) {
var colours = ["red", "blue", "indigo", "violet", "green"];
var colour = colours[Math.floor(Math.random() * colours.length)];
next(null, colour);
}, {
cache: {
expiresIn: 30000,
}
}
);

We also need to modify our route handler to pass in the name, as follows.

function(request, reply) {
var names = request.params.name.split("/");
server.methods.getColour(request.params.name, function(err, colour) {
reply({
first: names[0],
last: names[1],
mood: request.query.mood,
age: request.query.age,
colour: colour
});
});
}

Now, if we make a request to our /hello/{name*2} route, Hapi will first consult the cache to check if the name has a colour already cached for it. If it doesn’t, the server method’s function will be called, and the outcome will be cached for 30 seconds.

Restart the server and make a request to http://localhost:8080/hello/John/Doe. In my case, the result is the following:

{"first":"John","last":"Doe","mood":"neutral","age":53,"colour":"blue"}

Any subsequent requests with the name “John Doe” — for up to 30 seconds — will yield “blue” as the colour.

Views and Static Assets

We’ve been dealing solely with JSON-serialised data throughout this guide. Let’s bring some simple HTML views into the mix.

Hapi supports view rendering out of the box with any template engine. Jade seems very popular, so let’s use Jade to render our views. Install Jade via npm and create a directory, views. In the Server configuration object, add the following.

views: {
engines: {
jade: require("jade")
},
path: "./views"
}

This tells Hapi to render templates with the “.jade” extension using the jade module, and to look for views in “./views”. In the views directory, create a new file named “hello.jade” with the following contents.

doctype htmlhtml
head
title Hello, #{first} #{last}!
link(rel="stylesheet", href="//netdna.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap.min.css")
body
div.container
h1 Hey there, #{first}!
p.
According to my data, your name is #{first} #{last}.
You're #{age || "unknown"} years old, and I predict you're fond of the
colour #{colour}... Correct? At the moment you appear to
be #{mood}.
p That's all I know...

Modify the route handler to call the view rendering method of the reply interface.

reply.view("hello", {
first: names[0],
last: names[1],
mood: request.query.mood,
age: request.query.age,
colour: colour
});

Now, if we send a valid request to the route, we’ll get a view that looks like this.

We can also use Hapi to serve static assets. Create a new directory, public, and a sub-directory images.

First of all, we need to add a new route for “/static/{path*}”.

server.route({
path: "/static/{path*}",
method: "GET",
handler: {
directory: {
path: "./public",
listing: false,
index: false
}
}
});

This time around, our route’s handler is another configuration object rather than a function. The handler is specifying to serve a directory with the following options:

  • Resolve request.params.path relative to the ./public directory.
  • Disable directory listings.
  • Disable the serving of files beginning with index.html.

In the images directory, add an image for each valid mood — that’s “neutral”, ‘happy” and “sad”. For this tutorial, we’ll just stick to PNGs.

Update our “hello.jade” template to the following:

doctype htmlhtml
head
title Hello, #{first} #{last}!
link(rel="stylesheet", href="//netdna.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap.min.css")
body
div.container
h1 Hey there, #{first}!
p.
According to my data, your name is #{first} #{last}.
You're #{age || "unknown"} years old, and I predict you're fond of the
colour #{colour}... Correct? At the moment you appear to
be...
img(src="/static/images/#{mood}.png", width=200)
p That's all I know...

We now get a different image based on the mood.

Logging

Hapi logging is expressive and easy to use. Hapi.Server inherits from EventEmitter, and provides the following log consumption events.

  • log — events logged using the log method of the server.
  • request — events logged using the log method of a request object.

Tags

Throughout Hapi, we’ll come across tags. When logging with Hapi, tags allow us to easily deal with a particular type of event and differentiate between them. Tags are the only required identifier — log messages are entirely optional.

Logging Server-Related Events

Hapi.Server exposes a log method that takes the following parameters.

  • tags — array of tags used to identify the event.
  • data — an optional argument allowing us to pass a message or object along with the log.
  • timestamp — an optional timestamp associated with the log entry, expressed in milliseconds.

Hapi logs various internal events using this method. Here are some examples.

server.log(["test"], "This is my log entry!");
server.log(["error"], "Bogus data received from cache, unable to proceed.");

To consume these events, we will add a listener to the server for “log”.

server.on("log", function(event, tags) {
var tagsJoined = Object.keys(tags).join();
var message = event.data;
console.log("Log entry [" + tagsJoined + "] (" + (message || "") + ")");
});

Logging Request-Related Events

It’s also simple to associate logs with a particular request.

Let’s add a new route, and when we request it, it will invoke the log method of the request object.

server.route({
path: "/log/{data}",
method: "GET",
handler: function(request, reply) {
request.log(["pathData"]);
reply("Logged " + request.params.data);
}
});

To consume request-level logs, we can listen for the “request” event on our Hapi.Server instance, similarly to how we logged server-related events. In addition to the “event” and “tags” arguments, we also get access to the request object as the first parameter.

server.on("request", function(request, event, tags) {
if (tags.pathData) {
console.log("Logging pathData: " + request.params.data);
}
});

When we’re only interested in logging the acknowledgement of a request to the console, we can add a debug object to the server’s configuration (passed to the Hapi.Server constructor) that looks like the following.

debug: {
request: ["received"]
}

We’ll start to see data logged to stdout each time someone sends a request.

Debug: hapi, received 
{"id":"1394926567679-33146-48432","method":"get","url":"/","agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.1864.0 Safari/537.36"}

Plugins

Having every route and server configuration in one script can get unwieldy pretty quickly. Fortunately, Hapi provides a plugin interface that can be used to split our project up into logical components, or to quickly hook new functionality into our Hapi server from npm.

Before diving into plugin architecture, we need to understand that Hapi provides an interface for grouping servers together, called a “pack”. Every Hapi.Server belongs to a Hapi.Pack, regardless of whether or not it was explicitly added to one or not. We’ll explain this concept later on; for now, you only need to know that the plugin interface is built atop the pack interface.

For example, we can easily activate the “lout” plugin — developed by Spumko — to provide a “/docs” route, displaying the server’s routing table as a set of endpoints. Use npm to install lout, and replace the server.start() call with the following.

var lout = require("lout");

server.pack.register({plugin: lout}, function(err) {
if (err) throw err;
server.start(function() {
console.log("Hapi server started @ " + server.info.uri);
});
});

Navigate to http://localhost:8080/docs and we’ll receive a list of routes we have configured as well as the HTTP method they use. This functionality is added by lout — if you want to see configurable options, visit the GitHub repository.

Creating a Plugin

Plugins are regular modules that export a register function, called when the plugin is registered to a pack. Plugins can live in node_modules or any other directory resolvable by Node’s module system. npm modules can be required using just the name of the plugin (which we did with lout above), other modules need to be specified relative to the working directory of the server.

Create a “plugins” directory, and create a sub-directory named “example”. In this directory, run “npm init” as we did when we started the MyApp project.

bash-3.2$ mkdir -p plugins/example
bash-3.2$ cd plugins/example/
bash-3.2$ npm init
name: (example)
version: (0.0.0) 0.0.1
description: Example Hapi plugin
entry point: (index.js)
test command:
git repository:
keywords:
author: Fionn Kelleher <me@fionn.co>
license: (ISC) BSD
About to write to /Users/fionnkelleher/MyApp/plugins/example/package.json:

{
"name": "example",
"version": "0.0.1",
"description": "Example Hapi plugin",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Fionn Kelleher <me@fionn.co>",
"license": "BSD"
}

Is this ok? (yes)

In the directory, create index.js with the following contents.

exports.register = function(plugin, options, next) {
next();
};

exports.register.attributes = {
pkg: require("./package.json")
}

We’re exporting a register function that will be called as soon as the plugin has been required. It has the signature function(plugin, options, next) {…}.

The register function is the most important part, as it provides a means of configuring the plugin. To explain the arguments the function takes:

  • plugin — the plugin interface that we utilise to define routes, methods and more that are associated with the plugin.
  • options — an object containing any options passed when requiring the plugin.
  • next — a function that is required to be called when the plugin is considered to be in a usable state.

Let’s expand our barebones plugin to define a route.

Our plugin will now resemble the following:

exports.register = function(plugin, options, next) {
plugin.route({
path: "/my/plugin",
method: "GET",
handler: function(request, reply) {
reply("This is a reply from a route defined in my plugin!");
}
});
next();
};

exports.register.attributes = {
pkg: require("./package.json")
};

Modify MyApp’s index.js to require the plugin and register it.

server.pack.register([
{ plugin: require("lout") },
{ plugin: require("./plugins/example") }
]
, function(err) {
if (err) throw err;
server.start(function() {
console.log("Hapi server started @ " + server.info.uri);
});
});

Restart the server and send a request to http://localhost:8080/my/plugin and we’ll be greeted with “This is a reply from a route defined in my plugin!”.

Passing Configuration To Plugins

It’s possible to pass a configuration object to your plugins, for example if we were wishing to make the route’s path configurable, we could modify our plugin to the following:

var Hoek = require("hoek");var defaults = {
route: "/my/plugin"
};
exports.register = function(plugin, options, next) {
options = Hoek.applyToDefaults(defaults, options);

plugin.route({
path: options.route,
method: "GET",
handler: function(request, reply) {
reply("This is a reply from a route defined in my plugin!");
}
});
next();
};

exports.register.attributes {
pkg: require("./package.json")
};

Make sure you have the Hoek module installed as a dependency. Hoek is a utility library developed by Spumko, and includes an easy method to apply user configured options to an object of default values.

When we modify index.js to the following, the route will be changed to “/my/custom/route”.

server.pack.register([
{ plugin: require("lout") },
{
plugin: require("./plugins/example"),
options: { route: "/my/custom/route" }
}
], function(err) {
if (err) throw err;
server.start(function() {
console.log("Hapi server started @ " + server.info.uri);
});
});

Packs

We talked briefly about packs in the plugins section; we’re now going to look into them in a little more depth.

Hapi.Pack objects represent a group of servers, usually grouped in a logical manner. Packs allow us to easily share functionality between independent servers, as well as start and stop them with little effort. Each instance of Hapi.Server holds a reference to a pack. When we created the server for MyApp, a pack was automatically created for us and assigned to the “pack” property of the server.

More often than not though, packs are created explicitly as so:

var pack = new Hapi.Pack();

Packs implement methods to start and stop it, as well as the plugin interface we explored in the Plugins section earlier. They also allow us to create a new instance of Hapi.Server and assign it using the server method.

var s1 = pack.server(8080, "localhost");s1.route({
path: "/server/{id}",
method: "GET",
handler: function(request, reply) {
reply(request.params.id);
}
});
var s2 = pack.server(8081, "localhost");pack.register([
{ plugin: require("lout") },
{ plugin: require("./plugins/example") }
], function(err) {
if (err) throw err;
pack.start(function(server) {
console.log("Hapi pack started.");
});
});

We’re also assigning labels to the two servers — these are conceptually similar to tags.

Since our pack has the lout plugin loaded, we can request either http://localhost:8080/docs or http://localhost:8081/docs and get a nice view of each server’s routing table. You’ll notice that both servers also have the route provided by the plugin we created earlier, however, only the first server has the “/server/{id}” route.

Composing Packs

Packs of Hapi servers can be created using the “.compose()” method of Hapi.Pack. This allows us to use a configuration object to define each server’s attributes as well as what plugins to load.

The pack we created above could be written as the following:

var manifest = {
servers: [
{ port: 8080 },
{ port: 8081 }
],
plugins: {
"lout": {}
"./plugins/example": {
route: "/my/custom/route"
}
}
}
Hapi.Pack.compose(manifest, function(err, pack) {
pack.start(function() {
console.log("Servers started");
});
});

(Note: There appears to be a tiny bug in resolving relative plugins using Composer. To make this example run, you need to remove “./plugins/example” from the plugins array in our manifest.)

A rundown:

  • First we create a manifest object, specifying the configuration of our pack.
  • We call the compose method of Hapi.Pack to create the pack and any require our plugins.
  • The pack is then started.

Using the Hapi Executable

Although Hapi servers can be written entirely in JavaScript, Hapi provides an executable that allows you to compose and launch packs of servers from a JSON configuration identical to what we used before. This is useful for when your Hapi servers are split up into plugins.

First, create a JSON configuration in the root of MyApp named “manifest.json”, containing the following:

{
"servers": [
{
"port": 8080
},
{
"port": 8081
}
],
"plugins": {
"lout": {},
"./plugins/example": {
"route": "/my/custom/route"
}
}
}

To start our servers we can now execute “./node_modules/hapi/bin/hapi -c manifest.json” and our servers will be composed and started.

Environmentally Dependent Configuration

Configuration gets loaded via Confidence, a configuration document format interoperable with normal JSON. Confidence provides a few useful features to deal with configurations; one of which is the ability to modify the configuration at runtime based on other values either present in the configuration itself, or environmental variables. These are called filters.

To learn about Confidence, I recommend taking a read of Vawks Clamantis’ “Configuration with Confidence”, which you can find over here.

Unit Testing

For sake of brevity in this tutorial, I’ve created a whole new tutorial on unit testing Hapi servers. You can find it over here.

This getting started guide is only the tip of the iceberg for Hapi. Hapi is a pretty large framework, bundling a lot of functionality into an easy to use API. If you’re interested in diving in deeper, the reference is always a good place to start. Eran Hammer (hueniverse) has also done a recent presentation to &yet on Hapi 2.0, which can be found here. Since Hapi’s brisk development cycle results in frequent backwards-incompatible changes, it’s worth keeping up-to-date with the reference to make sure that if your app was built based on an older version of Hapi, it’s updated accordingly to mitigate any potential issues.

--

--

Fionn Kelleher

17. Programmer, student, programming student. Author of “Getting Started with Hapi.js” (O’Reilly).