Over a million developers have joined DZone.

hapi.js: Building Custom Route Handlers

DZone's Guide to

hapi.js: Building Custom Route Handlers

An excerpt from an upcoming book on hapi.js, and how to extend the framework by writing custom route handlers.

· Web Dev Zone
Free Resource

Learn how to build modern digital experience apps with Crafter CMS. Download this eBook now. Brought to you in partnership with Crafter Software

Image title

In this article, excerpted from hapi.js in Action, I’ll show you how to extend the framework by writing custom route handlers.

Handlers are where you declare what should actually happen when a request matches one of your routes. The most basic handler is just a JavaScript function with the signature:

function (request, reply) {...}

Some hapi plugins add their own handler types. An example of this is the directory handler for serving static content, added by the Inert plugin:

    method: 'GET',
    path: '/assets/{path*}',
    handler: {
        directory: {//#A
            path: Path.join(__dirname, 'assets')//#A

//#A Behavior of the route is defined in configuration using the directory handler

One of the central philosophies of hapi is that configuration is favourable over code. Configuration is usually easier to write, easier to read, easier to modify and reason about than the equivalent code.

If you find yourself repeating a common set of tasks or behaviour in your handlers, you could consider extracting a new custom handler type. Without further ado, let’s see an example.

The Internationalization (i18n) Example

In this example, we’re building a (very) small website. The website will cater to an international audience, so we want to include support for multiple languages from the start. Internationalization, also known as i18n, isn’t a feature that’s built into hapi so you’re going to create it yourself!

In this article, you’re going to see how you can write a custom handler to wrap up the complexity of this task into a simple-to-use handler.

The website, which is in its early stages of development, currently only has one page - the homepage. We have created a Handlebars template for that:


Ok, so when I called it a website I was probably overstating things. It’s just a single line of HTML that says hello - but it has potential!

We currently have a simple skeleton hapi application to serve this view:

const Hapi = require('hapi');
const Path = require('path');

const server = new Hapi.Server();
server.connection({ port: 4000 });

server.register(require('vision'), (err) => {//#A

    if (err) {
        throw err;

        engines: {//#B
            hbs: require('handlebars')//#B
        path: Path.join(__dirname, 'templates')//#B

            method: 'GET',
            path: '/',
            handler: {//#C
                view: 'index'//#C

    server.start(() => {

        console.log('Server started!');

// #A Load vision module (which adds view handler to hapi)
// #B Configure view engine
// #C Use the view handler to render the index template

We’ve decided to send off our Handlebars templates to translators. So we send them off to a French and a Chinese Translator. We also come up with a new naming scheme, suffixing the template name with the ISO 639-1 two letter language code. We now have three templates in total. They are named

templates/index_en.hbs (English template)

templates/index_fr.hbs (French template)

templates/index_zh.hbs (Chinese template)

Parsing the Accept-Language Header

Our application needs to look at an incoming request and decide which language-specific template it should serve, as shown in figure 1.

Image title

Figure 1 — The application should determine which template to use for a given request

The Accept-Language HTTP request header, when present, specifies the user’s preferred languages, each with a weighting or priority (called a “quality factor” in the HTTP spec, denoted by q). An example of an Accept-Language header is:

Accept-Language: da, en-gb;q=0.8, en;q=0.7

This can be translated into:

I would like this resource in Danish. If you don’t have Danish, I would like British English. If you don’t have British English, I will settle for any kind of English.

We can use a Node.js package, appropriately named accept (part of the hapi.js ecosystem), to help out parsing those headers into a more usable form. To see what kind of thing the accept-language module gives us back, you can run this one-liner (after running npm install --save accept in our project) in your terminal:

node -e "console.log(require('accept').languages('da, en-gb;q=0.8, en;q=0.7'))"

The output should be an array of language codes, sorted by user preference:

[ 'da', 'en-gb', 'en' ]

Our First implementation Attempt

We can now use our language-specific templates and knowledge of the Accept-Language header to build a naive implementation of our i18n-enabled hapi-powered website.

Image title

Figure 2 — Flow chart of process our app will use find a suitable template for a request

When a request is received, we want to check if we have a matching template for any of the languages in the Accept-Language header. If there’s no header present, or there are no matching templates, we will fall back to rendering the default language template. This process is shown in figure 2.

A quick implementation of this for a single route is shown below:

const Accept = require('accept');

        method: 'GET',
        path: '/',
        handler: function (request, reply) {

            const supportedLanguages = ['en', 'fr', 'zh'];//#A
            const defaultLanguage = 'en';//#A
            const templateBasename = 'index';//#A

            const langs = Accept.languages(request.headers['accept-language']);//#B//#B

            for (let i = 0; i < langs.length; ++i) {    //#C
                if (supportedLanguages.indexOf(langs[i]) !== -1) { //#C
                    return reply.view(templateBasename + '_' + langs[i]);//#C    //#C
                }    //#C
            }    //#C

            reply.view(templateBasename + '_' + defaultLanguage);    //#D

//#A Define some settings
//#B Parse the Accept-Language header
//#C Loop through each preferred language and if the current one is supported, render the appropriate view
//#D Otherwise, render the default language’s view

You can test this out, trying different Accept-Language headers, by sending some requests with cURL:

$ curl localhost:4000 -H "Accept-language: en"
<h2>Hello!</h2> #A

$ curl localhost:4000 -H "Accept-language: zh"

$ curl localhost:4000 -H "Accept-language: fr"
<h2>Bonjour!</h2> #C

$ curl localhost:4000 -H "Accept-language: de"
<h2>Hello!</h2> #D

#A en template picked
#B zh template picked
#C fr template picked
#D default (en) template picked

Making Things Simple Again

Although our first implementation works for sure, it’s pretty ugly and involves a lot of boilerplate code that needs to be copied into each of our handlers for any new routes we add. Do you remember how easy it was to use that view handler from Vision? That was a simpler time; we want to get back to that.

What we need to do then is to build a custom handler that can be used just like the view handler  and takes care of all the messy business behind the scenes for us. You create new custom handlers using the server.handler() method.

Your custom handler function will accept the route and the options given to it as parameters and should return a handler with the usual function signature.

server.handler('i18n-view', (route, options) => {

    const view = options.view;//#A

    return function (request, reply) {

        const settings = {//#B
            supportedLangs: ['en', 'fr', 'zh'],//#B
            defaultLang: 'en'//#B

        const langs = Accept.languages(request.headers['accept-language']); //#C

        for (let i = 0; i < langs.length; ++i) {      //#D
            if (settings.supportedLangs.indexOf(langs[i]) !== -1) {  //#D
                return reply.view(view + '_' + langs[i]); //#D
            }   //#D
        }   //#D

        reply.view(view + '_' + settings.defaultLang);   //#E

//#A View name is passed in through options
//#B Define some settings
//#C Parse the Accept-Language header
//#D Loop through each preferred language and if the current one is supported, render the view
//#E Otherwise, render the default language’s view

One improvement I would like to add to this is to remove the settings object from the handler. Having these explicit values in there tightly binds the custom handler to our individual usage. It’s a good idea to keep configuration like this in a central location.

When creating a hapi server you can supply an app object, with any custom configuration you would like. These values are then accessible inside server.settings.app, so let’s move the i18n configuration there:

const server = new Hapi.Server({
    app: {//#A
        i18n: {//#A
            supportedLangs: ['en', 'fr', 'zh'],//#A
            defaultLang: 'en'//#A


server.handler('i18n-view', (route, options) => {

    const view = options.view;

    return function (request, reply) {

        const settings = server.settings.app.i18n;//#B


//#A Specify application config when creating server
//#B Access same config later in `server.settings.app`

Now all there is to do is to use our shiny new custom handler, which is as simple as supplying an object with an i18n-view key and setting the template name.

        method: 'GET',
        path: '/',
        handler: {
            'i18n-view': {
                view: 'index'

We can reuse this handler now throughout our codebase without any ugly boilerplate code.

Exercise for the Reader

The view handler from the Vision plugin allows you to pass a context object, which will be used as the context when rendering the view.

Can you modify the i18n-view handler to also accept a context object? You should test this out by making a new view which outputs a variable supplied in the context:

    method: 'GET',
    path: '/sayHello',
    handler: {
        'i18n-view': {
            view: 'index',
            context: { name: 'steven' }

Crafter is a modern CMS platform for building modern websites and content-rich digital experiences. Download this eBook now. Brought to you in partnership with Crafter Software.

hapi.js ,javascript ,api development ,node.js

Opinions expressed by DZone contributors are their own.

The best of DZone straight to your inbox.

Please provide a valid email address.

Thanks for subscribing!

Awesome! Check your inbox to verify your email so you can start receiving the latest in tech news and resources.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}