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
- Get Your Server Up and Running
- v17 Upgrade Guide (Your Move to async/await)
- Become a Better Developer in 2018
- Become a Better Developer in 2017
- What You’ll Build
- Prepare Your Project: Stack & Structure
- Environment Variables and Storing Secrets
- Set Up MongoDB and Connect With Mongoose
- Sending Emails in Node.js
- Load the User’s Profile Picture From Gravatar Using Virtuals in Mongoose
- Implement a User Profile Editing Screen
- Generate a Username in Mongoose Middleware
- Displaying Seasons and Episodes for TV Shows with Mongoose Relationship Population
- Implementing Pagination for Movies
- Implement a Watchlist
- Create a Full Text Search with MongoDB
- Create a REST API with JSON Endpoints
- Update Mongoose Models for JSON Responses
- API Pagination for TV Shows
- Customize API Endpoints with Query Parameters
- Always Throw and Handle API Validation Errors
- Advanced API Validation With Custom Errors
- Create an API Documentation with Swagger
- Customize Your Swagger API Documentation URL
- Describe Endpoint Details in Your Swagger API Documentation
- 10 Tips on API Testing With Postman
- JWT Authentication in Swagger API Documentation
- API Versioning with Request Headers
- API Login With Username and Password to Generate a JWT
- JWT Authentication and Private API Endpoints
- Refresh Tokens With JWT Authentication
- Create a JWT Utility
- JWT Refresh Token for Multiple Devices
- Check Refresh Token in Authentication Strategy
- Rate Limit Your Refresh Token API Endpoint
- How to Revoke a JWT
- Invalidate JWTs With Blacklists
- JWT Logout (Part 1/2)
- JWT “Immediate” Logout (Part 2/2)
- A Better Place to Invalidate Tokens
- How to Switch the JWT Signing Algorithm
- Roll Your Own Refresh Token Authentication Scheme
- JWT Claims 101
- Use JWT With Asymmetric Signatures (RS256 & Co.)
- Encrypt the JWT Payload (The Simple Way)
- Increase JWT Security Beyond the Signature
- Unsigned JSON Web Tokens (Unsecured JWS)
- JWK and JWKS Overview
- Provide a JWKS API Endpoint
- Create a JWK from a Shared Secret
- JWT Verification via JWKS API Endpoint
- What is JOSE in JWT
- Encrypt a JWT (the JWE Way)
- Authenticate Encrypted JWTs (JWE)
- Encrypted and Signed JWT (Nested JWT)
- Bringing Back JWT Decoding and Authentication
- Bringing Back JWT Claims in the JWT Payload
- Basic Authentication With Username and Password
- Authentication and Remember Me Using Cookies
- How to Set a Default Authentication Strategy
- Define Multiple Authentication Strategies for a Route
- Restrict User Access With Scopes
- Show „Insufficient Scope“ View for Routes With Restricted Access
- Access Restriction With Dynamic and Advanced Scopes
- hapi - How to Fix „unknown authentication strategy“
- Authenticate with GitHub And Remember the Login
- Authenticate with GitLab And Remember the User
- How to Combine Bell With Another Authentication Strategy
- Custom OAuth Bell Strategy to Connect With any Server
- Redirect to Previous Page After Login
- How to Implement a Complete Sign Up Flow With Email and Password
- How to Implement a Complete Login Flow
- Implement a Password-Reset Flow
- Views in hapi 9 (and above)
- How to Render and Reply Views
- How to Reply and Render Pug Views (Using Pug 2.0)
- How to Create a Dynamic Handlebars Layout Template
- Create and Use Handlebars Partial Views
- Create and Use Custom Handlebars Helpers
- Specify a Different Handlebars Layout for a Specific View
- How to Create Jade-Like Layout Blocks in Handlebars
- Use Vue.js Mustache Tags in Handlebars Templates
- How to Use Multiple Handlebars Layouts
- How to Access and Handle Request Payload
- Access Request Headers
- How to Manage Cookies and HTTP States Across Requests
- Detect and Get the Client IP Address
- How to Upload Files
- Quick Access to Logged In User in Route Handlers
- How to Fix “handler method did not return a value, a promise, or throw an error”
- How to Fix “X must return an error, a takeover response, or a continue signal”
- Query Parameter Validation With Joi
- Path Parameter Validation With Joi
- Request Payload Validation With Joi
- Validate Query and Path Parameters, Payload and Headers All at Once on Your Routes
- Validate Request Headers With Joi
- Reply Custom View for Failed Validations
- Handle Failed Validations and Show Errors Details at Inputs
- How to Fix AssertionError, Cannot validate HEAD or GET requests
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.
- hapi v17 release notes
- Futureflix Starter Kit pull request with migration to haps v17
- make-promises-safe
- hapi-compat
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!