hapi — v17 Upgrade Guide (Your Move to async/await)

The release of hapi v17 marks a change of the framework towards modern Node.js features. With v17, hapi has full end-to-end support for async/await.

This major release is basically a rewrite of the hapi code base from callbacks to async/await. The recent changes in JavaScript and Node.js v8.x allows frameworks like hapi to implement new language features.

The newest release of hapi comes with breaking changes. This hapi v17 migration guide is your helping hand to get your v16 hapi app to the new major release.

This is a living document, we’ll continuously update and extend it. The following sections help you with your hapi v17 migration.

hapi Series Overview

Switch to a new Branch

First of all, use a new branch in your version control before starting any of the migration work.

Keep the migration state of your project in a different branch. This way, you don’t affect the stability of your project and force your coworkers to upgrade their system before they are ready to make the move.

# switch to your develop branch first …
git checkout develop

# … and move to the hapi migration v17 branch from there
git checkout -b hapi-v17  

Switched to a new branch? Alright, let’s move on 😃

Update the Project Dependencies

Due to the major changes in hapi’s core, all plugins from hapi’s ecosystem require an update as well.

The plugins need to follow a new structure. Check your dependencies in the package.json file and update them to their newest major releases. The following list illustrates hapi plugins that you might depend on.

Notice: the dependencies list already uses the final release versions of each package. At the time of your update, you might need to use the release candidate versions.

package.json

"dependencies": {
  "hapi": "~17.0.1",
  "boom": "~7.1.0",
  "hoek": "~5.0.2",
  "inert": "~5.0.1",
  "joi": "~13.0.1",
  "vision": "~5.1.0",
  "hapi-dev-errors": "~2.0.1",
  "hapi-geo-errors": "~3.0.0"
  …
},
"devDependencies": {
  "code": "~5.1.2",
  "lab": "~15.1.2"
}

If you don’t depend on any library besides hapi, you’re fine by updating hapi. Use npm outdated to find the packages with updates.

In case you’re using hapi plugins like vision, inert or hapi-dev-errors, you need to update them to their latest release to be compatible with hapi v17.

If you don’t have the v17 compatible version of plugins installed, your hapi server instance throws an error. There’s a section later in this migration guide that shows you how hapi plugins should be structured for v17.

A Single Connection Per Server

With hapi v17 you’ve a single connection per server. Previously, you could add multiple connections on different ports. Like, splitting the code for frontend and API was seamlessly possible until v16. It still is in hapi v17, but needs a different handling.

If you see this error when starting your hapi project with v17, check and refactor each server.connection.

server.connection({  
       ^

TypeError: server.connection is not a function  

In hapi v17, you need to initialize the connection details, like host and port, with the server’s constructor. The server.connection() method is not available anymore.

hapi v17

// create new server instance and connection information
const server = new Hapi.Server({  
  host: 'localhost',
  port: 3000
})

If you want multiple connections in your project, create multiple server instances with Hapi.Server({ … }).

hapi v16

// create new server instance
const server = new Hapi.Server()

// add server’s connection information
server.connection({  
  host: 'localhost',
  port: 3000
})

In hapi v16 (and lower), you must call server.connection() to define the connection details. Each call returns a server instance and calling it multiple times with different connection settings allows you to create individual server instances, e.g., to serve frontend and API separately.

Register Plugins

The hapi core is a minimal set of features to run a web application. Make use of plugins to extend the framework. Register plugins to a hapi server instance and they hook into the request lifecycle, decorate interfaces (e.g., request or server) or do different tasks.

When moving to hapi v17, you might run into the following issue:

UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1): Error: Invalid register options "value" must be an object  

This is due to the changed plugin structure in v17. The server.register method is still existing, but you don’t provide a function with attributes using the register key rather than a plugin object.

With v17, you need to await the asynchronous plugin registration process. You can either do that by wrapping your plugin registration into an async function or use the returned promise.

hapi v17

// create new server instance
const server = new Hapi.Server({  
  port: 3000
})

// register plugins, wrapped in async/await
async function liftOff () {  
  await server.register({
    plugin: require('vision')
  })

  server.views(…)
}
liftOff()

// or

// register plugins using a promise
server.register({  
  plugin: require('vision')
}).then(() => { server.views(…) })

In the previous hapi versions, you either provided an error callback or a promise. The callback is not an option anymore. You need to move to async/await or a resolving promise with .then().

hapi v16

// create new server instance
const server = new Hapi.Server()

// add server’s connection information
server.connection({  
  host: 'localhost',
  port: 3000
})

// register plugins, callback style
server.register({  
    plugin: require('vision')
}, err => { server.views(…) })

// or

// register plugins using a promise (yep, it’s supported in hapi v16)
server.register({  
  plugin: require('vision')
}).then(() => { server.views(…) })

You don’t need to change anything for the server.register call when using promises in v16 already. The finished plugin registration resolves a promise and you can chain the .then() to proceed with further configurations or the server start.

Create Plugins and Their Exports

Plugins play an essential role in hapi to extend the framework with custom functionality. For the new major release, you need to replace exports.register and exports.register.attributes with exports.plugin = { register, name, version, multiple, dependencies, once, pkg }.

If you’re running into the following error, it’s an indicator that a plugin is incompatible with hapi v17 due to the breaking structure change:

UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1): Error: Invalid register options "value" must be an object  

To comply with the new structure, update your plugins to use a named export plugin that provides an object containing all the information. At least the register function that takes the server and options object.

No more next() at the end of your plugin registration. Hapi v17 registers all plugins asynchronously and the core waits for each plugin to return a value. If you don’t return anything in your plugin, hapi takes it as a „ready“ signal. At the time every plugin resolves, it tells hapi to proceed with the server startup.

hapi v17

exports.plugin = {  
  register: (server, options) => {
    …
  },
  pkg: require('../package.json')
}

Remember that you can specify the name, version, multiple, dependencies, once fields in the named export of exports.plugin besides register and pkg.

In v16, you need to export the register method and the meta data object for register.attributes.

hapi v16

exports.register = (server, options, next) => { … }

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

The new plugin structure in v17 merges the meta data with the actual plugin functionality. This change makes clear what needs to be exported from a plugin. Nice move!

Directly Return in Route Handlers, No More “reply()” Callback

With hapi v17 you can return values from route handlers directly. The reply interface isn’t available anymore. It was more of a callback with extra functionality that you could use to create, change and hold a response, before sending it.

Return a Response From Route Handlers

The new way to respond a request is to directly return a value from the route handler. You’ve access to a new response toolkit, which you’ll find as h within the hapi documentation.

Hapi’s response toolkit is powerful and gives you control to create a response. Leverage the new response toolkit to send a redirect or create your desired response.

The following code block shows some response samples and how to use the h response toolkit.

hapi v17

// the new structure applies to lifecycle points and request handlers
// here are some examples on how to use the new response toolkit

server.ext('onPreResponse', (request, h) => { … })

const handler = (request, h) => {  
  // return a string
  return 'ok'

  // return an object and hapi creates JSON out of it
  return { name: 'Future Studio', makeItRock: true }

  // redirect … to 404 … hehehehe :D
  return h.redirect('/404')

  // return a view
  return h.view('index', { name: 'Future Studio' })

  // use the "h" response toolkit to create a response
  return h
    .response(someHTML)
    .type('text/html')
    .header('X-Custom', 'my-value')
    .code(201)
}

In hapi v16, you had access to the reply interface which was a powerful callback method that returned a response object. This response was chainable for further manipulation.

hapi v16

server.ext('onPreResponse', (request, reply) => { … })

const handler = (request, reply) => {  
  // return a string
  return reply('ok')

  // return an object and hapi creates JSON out of it
  return reply({ name: 'Future Studio', makeItRock: true })

  // redirect … to 404 … hehehehe :D
  return reply.redirect('/404')

  // return a view
  return reply.view('index', { name: 'Future Studio' })

  // use the "reply" to create a response with chained methods
  return reply(someHTML)
    .type('text/html')
    .header('X-Custom', 'my-value')
    .code(201)
}

The new response toolkit provides most of the functionality that you had access to with the reply interface. Two methods aren’t part of hapi anymore: reponse.hold() and response.send().

Response Flow Control: removed “hold()” and “send()”

When calling reply() in hapi v16 and lower, the return value was the response object. You could customize it by adding headers or updating the status code. The response object was chainable to update the response with a single statement.

In hapi v17, the response toolkit handles the same job and returns a response object when calling h.response(). Return the response when ready.

hapi v17

const handler = (request, h) => {  
    const response = h.response('success')

    return new Promise(resolve => {
      setTimeout(() => {
        resolve(response)
      }, 1000)
    })
}

In case you created a response object in hapi v16 and wanted to execute long-running jobs asynchronously, you needed to use the .hold() method on the response object. This was the way to prevent an early response.

hapi v16

const handler = (request, reply) => {  
    const response = reply('success').hold()

    setTimeout(() => {
        response.send()
    }, 1000)
}

Both, response.hold() and response.send() aren’t necessary anymore. With async/await, you’re in control of the response flow.

onCredentials: a new Request Extension Point

Each request served with hapi follows a predefined path: the request lifecycle. Depending on whether you need authentication or validation, the framework skips individual lifecycle points.

There’s a new extension point in hapi v17: onCredentials. This extension point locates after onPreAuth and before onPostAuth. In onPreAuth, hapi authenticates the request and identifies the user. The authorization is part of onPostAuth, like checking the request scope to verify that the request has access rights.

In onCredentials, you can customize the credentials before the request authorization.

hapi v17

server.ext('onPreAuth', (request, h) => { … })  
server.ext('onCredentials', (request, h) => { … })  
server.ext('onPostAuth', (request, h) => { … })  

hapi v16

server.ext('onPreAuth', (request, reply) => { … })  
server.ext('onPostAuth', (request, reply) => { … })  

The near future will show how developers use the new extension point.

Start Your hapi Server

The server.start method is fully async in hapi v17. Don’t wait for an error callback, it won’t come. You can either use an async function to await the server start or make use of promises.

hapi v17

try {  
  await server.start()
}
catch (err) {  
  console.log(err)
}

// or use a promise
server  
  .start()
  .then(() => { … }) // if needed
  .catch(err => {
    console.log(err)
  })

Actually, you can use promises to start your hapi server in v16 already. Besides that, the error callback is available.

hapi v16

server.start(err => {  
  console.log(err)
})

// or use a promise
server  
  .start()
  .then(() => { … }) // if needed
  .catch(err => {
    console.log(err)
  })

Well, the server.start() has a companion and you guessed right. The server.stop() method changes as well.

Stop Your hapi Server

To stop your hapi server accordingly, close existing connections. With v17 you need to use an async function or a promise. There’s no callback anymore.

hapi v17

try {  
  await server.stop()
}
catch (err) {  
  console.log(err)
}

// or use a promise
server  
  .stop()
  .catch(err => {
    console.log(err)
  })

Same as with server.start(), you can use the promised-based server.stop() in hapi v16 already.

hapi v16

server.stop(err => {  
  console.log(err)
})

// or use a promise
server  
  .stop()
  .catch(err => {
    console.log(err)
  })

If you want to run functionality after the server stopped, chain an .then() to server.stop().

Route “config” is now “options”

Features like validation and handling for failed validation are part of a dedicated route configuration. You need to specify an object with the functionality, like validate. You can also add the handler in the configuration.

In hapi v17, this object changes its name from config to options. The config key will still work, but is deprecated.

hapi v17

server.route({  
  method: 'GET',
  path: '/',
  options: { … }
})

// works, but deprecated
server.route({  
  method: 'GET',
  path: '/',
  config: { … }
})

The older versions of hapi only support the config object.

hapi v16

server.route({  
  method: 'GET',
  path: '/',
  config: { … }
})

Check your project for routes that make use of the config object. Routes with validation will use it. Keep your project future proof and replace config with options.

Update “failAction”

With hapi, you’re in full control of validation errors. The failAction method gives you anything to respond appropriately to a situation where incoming data don’t comply the requested format.

Configure failAction in v17 to either ignore, log, error or a lifecycle method with the signature async function (request, h, error).

Reply Views from failAction

If you provide a lifecycle method for failAction, you’re running it before the handler. In this situation, hapi allows you to return an error, a takeover response or a continue signal.

For web requests from views, you want to render the view again with error details so that the user can react on the new situation. To respond with a view from failAction, use the .takeover() method for any toolkit call.

hapi v17

failAction: (request, h, error) => {  
  // e.g. remember the user’s email address and pre-fill for comfort reasons
  const email = request.payload.email

  return h
    .view('signup', {
      email,
      error: 'The email address is already registered'
    })
    .code(400)
    .takeover()    // <-- this is the important line
}

Using .takeover() will take your response directly to the response validation in the request lifecycle.

In the previous hapi version, you could respond a request without the takeover.

hapi v16

failAction: (request, reply, source, error) => {  
  // e.g. remember the user’s email address and pre-fill for comfort reasons
  const email = request.payload.email

  return reply
    .view('signup', {
      email,
      error: 'The email address is already registered'
    })
    .code(400)
}

Keep an eye on your route handlers with validation. In case you want to respond a web view, use the .takeover() method to not run into errors when replying from failAction.

“source” Is Part of the Error Argument

The response toolkit h replaces the reply interface which allows you to respond requests accordingly. The source —where the validation error occurs— moves into the error object.

hapi v17

server.route({  
  method: 'POST',
  path: '/login',
  options: {
    handler: (request, h) => {},
    validate: {
      payload: { … },
      failAction: (request, h, error) => {
        console.log(error.source) // --> logs "payload"
      }
    }
  }
})

In hapi v17, you’ll have three parameters for failAction whereas hapi v16 had four. The error’s source was a separate parameter.

hapi v16

server.route({  
  method: 'POST',
  path: '/login',
  config: {
    handler: (request, reply) => {},
    validate: {
      payload: { … },
      failAction: (request, reply, source, error) => {
        console.log(source) // --> logs "payload"
      }
    }
  }
})

Check the validations at your routes and update the failAction to support the new response toolkit h and the updated signature where the source object moves into the error object. The error object is still a boom object.

Set Default Authentication Strategy

Authentication strategies in hapi define a named instance of an authentication scheme. In hapi v16 and lower, you had the option to define an authentication mode while creating a strategy.

This authentication mode tells hapi to require this strategy for every route in your server or to be optional.

With hapi v17, there’s no more server.auth.strategy() where you can set the auth mode as the third parameter. You have to use server.auth.default() instead.

hapi v17

// create an auth strategy using hapi-auth-cookie
server.auth.strategy('session', 'cookie', {  
  …
}

// set "try" as default mode for the created "session" strategy
server.auth.default({  
  mode: 'try',
  strategy: 'session'
})

// use a strategy name to set it as a "required" default
server.auth.default('session')  

The server.auth.default() function takes either the strategy name as a string or an object with the specific mode and strategy.

Hapi v16 allowed you to register an authentication strategy and simultaneously set the strategy mode in a single statement.

hapi v16

// create an auth strategy using hapi-auth-cookie
// set "try" as default mode for "session"
// all-in-one :)
server.auth.strategy('session', 'cookie', 'try', {  
  …
}

// set "session" as the "required" authentication strategy
server.auth.strategy('session', 'cookie', 'required', {  
  …
}

Your code in hapi v17 will be more expressive due to the separation of strategy creation and setting the corresponding mode.

Update Your Tests to “await server.inject”

The move to async/await in hapi v17 involves updates to your tests. You need to await server.inject and wrap it into an async function.

To support testing with lab (and code) in hapi v17, install their latest major releases. Both plugins comply the new plugin structure for hapi v17 in their update-to-date releases.

Lab.test doesn’t return the done callback anymore, you actually don’t need to return anything. If you through an error or there is an assertion issue, the test will fail. Else, lab handles the test as a success.

hapi v17

const Lab = require('lab')

Lab.test('await server.inject', async () => {  
  const options = {
    url: '/',
    method: 'GET'
  }

  const response = await server.inject(options)
  const payload = response.payload
})

Notice the async keyword upfront the test method. Adding async let’s you use await statements and tells Node.js to handle this function appropriately in the control flow.

With hapi v16 and lab v14, you had to call the done() callback and server.inject returned a callback with the actual response.

hapi v16

const Lab = require('lab')

Lab.test('server.inject as callback', done => {  
  const options = {
    url: '/',
    method: 'GET'
  }

  server.inject(options, response => {
    const payload = response.payload

    done()
  })
})

Updating your tests might take some time. We’ve found that the code is much more readable after the migration to hapi v17 and you’ll remove some lines as well.

hapi v16 Stays Around for a While

You don’t need to make to jump to v17 now. As long as there is a Node.js LTS supporting hapi v16, there will be security updates. You can keep the v16 dependency in your package.json (at least) until April 2018. That’s the EOL (end of life) for Node.js v4 LTS and hapi v16 requires Node.js >= 4.8.0.

Helping Hands for Your hapi v17 Migration

Here are links that might help you with the hapi update. The hapi v17 release notes a safe place to scan through all changes.

The next link is a pull request for the Futureflix Starter Kit that contains all code changes for the async/await updates. We hope this helps you to relate in case of problems when migrating your app.

With make-promises-safe, you can find promise issues that are hard to find. If you notice a lot of unhandledRejection, check out this helper.

The hapi-compat plugin tries to detect, warn and auto-fix hapi v17 breaking changes in your project.

Outlook

This migration guide walks you through the steps to bump your hapi app to the newest major release. You can already see that there are breaking changes that need your attention. Notice that your migration might take some time.

Don’t rush through the changes and update your code in lack of concentration. Have fun migrating 🤘

We’re happy to hear your thoughts and comments. Please don’t hesitate to use the comments below or find us on Twitter @futurestud_io. Let us know what you think!

Enjoy coding & make it rock!


Mentioned Resources

Explore the Library

Find interesting tutorials and solutions for your problems.