Overview
This Express to hapi guide will show you how to take what you know how to do in Express, and do it in hapi. While Express relies heavily on middleware for much of its functionality, hapi has more built into the core. Body parsing, cookie handling, input/output validation, and HTTP-friendly error objects are already built-in to the hapi framework. For additional functionality, hapi has a robust selection of plugins in its core ecosystem. hapi is also the only framework that doesn't rely on outside dependencies. Every dependency is managed by the core hapi team, which makes security and reliability some of hapi's greatest strengths.
Setup
Installation
Express:
npm install express
hapi:
npm install @hapi/hapi
Creating a Server
Express:
var express = require('express');
var app = express();
app.listen(3000, function () {
console.log('Server is running on port 3000')
}))
hapi:
const Hapi = require('@hapi/hapi');
const init = async () => {
const server = Hapi.server({
port: 3000,
host: 'localhost'
});
await server.start();
console.log('Server running on port 3000');
};
init();
Unlike Express, in hapi you create a server object that will be the focal point of your application. The properties set in the server object will determine how your application behaves. Once you create your server object, you can start your server by calling
server.start()
.Routes
Routes in hapi get called in a specific order, so you will never have an issue where two routes are conflicting with one another. Routes are called from most specific to least specific. For example, a route with a path
'/home'
will be called before '/{any*}'
.
Lets look at how to set up a basic route in hapi:
Express:
app.get('/hello', function (req, res) {
res.send('Hello World!');
});
hapi:
server.route({
method: 'GET',
path:'/',
handler: (request, h) => {
return 'Hello World!';
}
});
To create a route, Express has the structure of
app.METHOD(PATH, HANDLER)
and hapi has the structure server.route({METHOD, PATH, HANDLER})
. The method, path, and handler are passed to the hapi server as an object. As you can see, to return a string in Express, you call res.send()
, whereas in hapi, you simply return the string.Methods
hapi can use all the route methods that Express can, except
HEAD
. hapi also has the ability to use multiple methods on a single route object. For example:server.route({
method: ['PUT', 'POST'],
path: '/',
handler: function (request, h) {
return 'I did something!';
}
});
To use all available methods, like in Express
app.all()
, use method: '*'
.Path
Like in Express, the path option in hapi must be a string, which can also contain parameters. Parameters in Express are preceded by
:
, such as: '/users/:userId'
. In hapi, you would put the parameter in curly braces, like: path: '/users/{userId}'
.Parameters
You saw above how hapi handles simple parameters as compared to Express. Both hapi and Express handle optional parameters the same way. Just like Express, to make a parameter optional in hapi, just include a
?
after the parameter: path: '/hello/{user?}
.
Accessing the parameters in hapi is very similar to Express. As you know, in Express, the parameters are populated in the
req.params
object. In hapi, the parameters are available via the request.params
object. Here is an example of both:
Express:
app.get('/hello/:name', function (req, res) {
const name = req.params.name
res.send('Hello ' + name);
});
hapi:
server.route({
method: 'GET',
path: '/hello/{name}',
handler: function (request, h) {
const name = request.params.name;
return 'Hello ' + name
}
})
Query parameters are also similar in both frameworks. In Express, the are available via
req.query
and hapi they are available via request.query
.Handler
There are differences in the way Express and hapi structure their route handlers. Unlike Express, which has a handler with parameters of
req
and res
, hapi has a handler with parameters of request
and h
. The second parameter, h
is the response toolkit, which is an object with several methods used to respond to the request.
Here is an example of route with a handler that redirects to another route in Express and hapi:
Express:
app.get('/home', function (req, res) {
res.redirect('/');
});
hapi:
server.route({
method: 'GET',
path: '/home',
handler: function (request, h) {
h.redirect('/');
}
});
Both routes will redirect to the
'/'
route. Express uses the response method res.redirect
whereas hapi uses h.redirect
which is part of the response toolkit. There are Express response methods that hapi can accomplish by just using return
. Some of these methods include res.send
and res.json
. Here is an example of how hapi will respond with JSON data:server.route({
method: 'GET',
path: '/user',
handler: function (request, h) {
const user = {
firstName: John,
lastName: Doe,
userName: JohnDoe,
id: 123
}
return user;
}
});
hapi has the functionality to respond with JSON data by default. They only thing you have to do is just return a valid JavaScript object and hapi will take care of the rest for you.
Middleware vs Plugins and Extensions
To extend its functionality, Express uses middleware. Middleware essentially is a sequence of functions using callbacks to execute the next function. The issue with this is as your application grows in size and complexity, the order at which middleware executes becomes more crucial and more difficult to maintain. Having a middleware execute before one it's dependant on will cause you application to fail. hapi fixes this issue with its robust plugin and extension system.
Plugins allow you to break your application logic into isolated pieces of business logic, and reusable utilities. Each plugin comes with its own dependencies which are explicitly specified in the plugins themselves. This means you don't have to install dependencies yourself to make your plugins work. You can either add an existing hapi plugin, or write your own. For a more extensive tutorial on plugins, please see the plugins tutorial.
Each request in hapi follows a predefined path, the request lifecycle. hapi has extension points that let you create custom functionlity along the lifecycle. Extension points in hapi let you know the precise order at which you application will run. For more info, please see the hapi request lifecycle.
Extension Points
hapi has 7 extension points along the request lifecycle. In order, they are
onRequest
, onPreAuth
, onCredentials
, onPostAuth
, onPreHandler
, onPostHandler
, and onPreResponse
. To add a function to an extension point, you call server.ext()
. Lets look at an example:server.ext('onRequest', function (request, h) {
request.setUrl('/test');
return h.continue
});
This function will run at
onRequest
, which is the first extension point. onRequest
is run just after the server receives the request object, before the route lookup. What this function will do is reroute all requests to the '/test'
route.Creating a Plugin
As you know, you can write you own middleware in Express. The same is true with hapi plugins. A plugin is an object with with required
name
and register
properties. The register
property is a function with the signature of async function (server, option)
. Lets look at how to create a basic plugin:
Express:
const getDate = function (req, res, next) {
req.getDate = new Date();
next()
}
hapi:
const getDate = {
name: 'getDate',
version: '1.0.0',
register: async function (server, options) {
const currentDate = function() {
const date = new Date();
return date
}
server.decorate('toolkit', 'getDate', currentDate);
}
}
The hapi plugin will save the current date in
h.getDate()
. We can then use this in any of our route handlers.Loading a Plugin
In Express, you load middleware by calling the
app.use()
method. In hapi, you call the server.register()
method. Lets load the plugin we created in the previous section:
Express:
app.use(getDate);
hapi:
server.register({
plugin: getDate
})
You can all addition options for you plugin by setting the
options
property on server.register()
.Options
You can add options to Express middleware by exporting a function that accepts an options parameter, which then returns the middleware. In hapi, you set the options when you register the plugin. Lets have a look:
Express:
module.exports = function (options) {
return function (req, res, next) {
req.getDate = 'Hello ' + options.name + ', the date is ' + new Date();
next()
}
}
hapi:
server.register({
plugin: getDate,
options: {
name: 'Tom'
}
})
To get access to the options in hapi, you simply refer to the
options
object when you create the plugin:
Express:
const getDate = require('./mw/getDate.js');
app.use(getDate({ name: Tom }));
hapi:
const getDate = {
name: 'getDate',
version: '1.0.0',
register: async function (server, options) {
const currentDate = function() {
const date = 'Hello ' + options.name + ', the date is ' + new Date();
return date
}
server.decorate('toolkit', 'getDate', currentDate);
}
}
body-parser
hapi has parsing abilities built into its core. Unlike Express, you do not need middleware to parse payload data. In fact, you may need to install up to four additional middlewares in Express depending on what kind of data you would like to parse. In hapi the payload data, whether its JSON or plain text, is readily available in the
request.payload
object. Here is a side by side comparison of parsing simple payload data:
Express:
var bodyParser = require('body-parser');
app.use(bodyParser.urlencoded({extend: true}));
app.post('/hello', function (req, res) {
var name = req.body.name
res.send('Hello ' + name);
});
hapi:
server.route({
method: 'POST',
path: '/hello',
handler: function (request, h) {
const name = request.payload.name
return `Hello ` + name;
}
});
To parse a JSON object in express, you have to specify it:
app.use(bodyParser.json())'
JSON parsing is built into hapi, so there are no further steps needed.
cookie-parser
Setting and parsing cookies in Express require you to install the
cookie-parser
middleware. hapi has cookie functionality built right into the core, so there is no need to install additional middleware. To use cookies in hapi, you first configure the cookie with server.state()
. Lets have a look:const Hapi = require('@hapi/hapi');
const server = Hapi.server({ port: 8000 });
server.state('data', {
ttl: null,
isSecure: true,
isHttpOnly: true
});
Setting a Cookie
Once the cookie is configured, you can now set the cookie with
h.state()
. Here is an example:
Express:
var express = require('express');
var app = express();
var cookieParser = require('cookie-parser);
app.use(cookieParser());
app.get('/', function(req, res) {
res.cookie('username', 'tom', { maxAge: null, secure: true, httpOnly: true})
res.send('Hello');
});
hapi:
const Hapi = require('@hapi/hapi');
const server = Hapi.server({ port: 8000 });
server.state('data', {
ttl: null,
isSecure: true,
isHttpOnly: true
});
server.route({
method: 'GET',
path: '/',
handler: function (request, h) {
h.state('data', {username: 'tom'});
return h.response('Hello');
}
});
In express, you configure cookie with the
options
object in res.cookie
. In hapi, the cookie config is saved to the server object with server.state
. You then use h.state()
to attach data to the cookie.Getting a Cookie Value
To get a cookie value in hapi, you call
request.state
. Lets have look:
Express:
var express = require('express');
var app = express();
var cookieParser = require('cookie-parser);
app.use(cookieParser());
app.get('/', async (req, res) => {
await res.cookie('username', 'tom', { maxAge: null, secure: true, httpOnly: true})
await res.send(req.cookies.username);
});
hapi:
const Hapi = require('@hapi/hapi');
const server = Hapi.server({ port: 8000 });
server.state('data', {
ttl: null,
isSecure: true,
isHttpOnly: true
});
server.route({
method: 'GET',
path: '/',
handler: async (request, h) => {
await h.state('data', {username: 'tom'});
return h.response(request.state.username);
}
});
Passport -> bell
In Express, third party authentication is handled with Passport. In hapi, you use the bell module for third party authentication.
bell
has over 30 predefined configurations for OAuth providers including Twitter, Facebook, Google, Github, and more. It will also allow you to set up your own custom provider. For a complete list, please see the bell providers documentation. bell
was developed and is maintained by the core hapi team, so you know stability and reliability won't be an issue. Lets look how to authenticate using your Twitter credentials:
Express:
npm install passport passport-twitter
var passport = require('passport');
var TwitterStrategy = require('passport-twitter').Strategy
passport.user(new TwitterStrategy({
consumerKey: TWITTER_CONSUMER_KEY,
consumerSecret: TWITTER_CONSUMER_SECRET,
callbackURL: '/auth/twitter/callback'
},
function(token, tokenSecret, profile, cb) {
User.findOrCreate({ twitterId: profile.id }, function (err, user) {
return cb(err, user);
}
}
));
passport.seralizeUser(function(user, cb) {
cd(null, user);
})
passport.deserializeUser(function(user, cb) {
cd(null, obj);
})
app.get('/auth/twitter', passport.authenticate('twitter'));
app.get('/auth/twitter/callback', passport.authenticate('twitter', { failureRedirect: '/login'}),
function(req, res) {
res.redirect('/');
});
hapi:
npm install '@hapi/bell'
const Hapi = require('@hapi/hapi');
const Bell = require('@hapi/bell');
const server = Hapi.server({ port: 8000 });
await server.register(Bell);
server.auth.strategy('twitter', 'bell', {
provider: 'twitter',
password: 'cookie_encryption_password_secure',
clientId: TWITTER_CONSUMER_KEY,
clientSecret: TWITTER_CONSUMER_SECRET,
isSecure: false
});
server.route({
method: '*',
path: '/auth/twitter', // The callback endpoint registered with the provider
handler: function (request, h) {
if (!request.auth.isAuthenticated) {
return `Authentication failed due to: ${request.auth.error.message}`;
}
// Perform any account lookup or registration, setup local session,
// and redirect to the application. The third-party credentials are
// stored in request.auth.credentials. Any query parameters from
// the initial request are passed back via request.auth.credentials.query.
return h.redirect('/home');
},
options: {
auth: 'twitter'
}
});
To use bell, simply register the plugin and configure the strategy with
server.auth.strategy
.provider
is the name of the third-party provider.password
is the cookie encryption password.clientId
is the OAuth client identifier, which is available from the provider.clientSecret
is the OAuth client secret, which is available from the provider.isSecure
sets the cookie secure flag. For production, this should be set to true
, which is the default value.express-validator -> joi
To validate data in Express, you make use of the
express-validator
plugin. One of the biggest drawbacks to express-validator
is that while you can validate a request, there is no clear way of validating a response. In hapi, you use the joi module, which can validate requests and responses with ease. Joi allows you to create your own validations with a simple and clean object syntax. For a more in-depth look at validation in hapi, please see the validation tutorial.Input Validation
Input validation allows you to validate any input data coming into the server, whether its parameters, payload, etc. Here is a look at how to validate a blog post entry in Express and hapi:
Express:
npm install express-validator
const bodyParser = require('body-parser');
const expressValidator = require('express-validator');
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
app.use(expressValidator())
app.post('/post', function (req, res) {
req.check('post', 'Post too long').isLength({ max: 140 });
let errors = req.validationErrors();
if (errors) {
res.send(errors);
} else {
res.send('Blog post added!')
}
});
hapi:
npm install @hapi/joi
const Joi = require('joi')
server.route({
method: 'POST',
path: '/post',
handler: async (request, h) => {
return 'Blog post added!';
},
options: {
validate: {
payload: {
post: Joi.string().max(140)
}
}
}
});
First you install
joi
, then require it in your project. To validate the input date, you specify what data type you are expecting, then set rules on that data. In this case, post
will be a string
with a maximum number of characters of 140. In joi
you can string rules together like:Joi.string().min(1).max(140).
Output Validation
As stated above, there is no clear way of doing response validation with
express-validator
. With joi
, response validation is fast and simple. Lets have a look:
hapi:
const bookSchema = Joi.object({
title: Joi.string().required(),
author: Joi.string().required(),
isbn: Joi.string().length(10),
pageCount: Joi.number(),
datePublished: Joi.date().iso()
});
server.route({
method: 'GET',
path: '/books',
handler: async function (request, h) {
return await getBooks();
},
options: {
response: {
schema: Joi.array().items(bookSchema),
failAction: 'log'
}
}
});
This route will return a list of books. In the
options
route property, we can specify what rules the list of books should follow. By setting the failAction
to log
, if there is an error, the server will log the error.app.set('view engine') -> vision
hapi has extensive support for template rendering, including the ability to load and leverage multiple templating engines, partials, helpers (functions used in templates to manipulate data), and layouts. Express enables views capabilities by using
app.set('view engine)
, where hapi's capabilities are provided by the vision plugin. For a more extensive tutorial on views in hapi, please see the viewstutorial.Setting the View Engine
Setting the views engine in express looks like the following:
app.set('view engine', 'pug');
To set the views engine in hapi, you first must register the vision plugin, then configure
server.views
:await server.register(require('vision'));
server.views({
engines: {
pug: require('pug')
},
relativeTo: __dirname,
path: 'views'
});
By default, Express will look for views or templates in the
views
folder. In hapi, you specify where the views are located using the relativeTo
and path
properties. Like Express, hapi supports a wide variety of templating engines, such as pug, ejs, handlebars, etc.
hapi has many more configurable options in
server.views
. To see the full list of capabilities, please go to the views tutorial.Rendering a View
To render a view in Express, you would call
First, rendering a view in Express:
res.render()
. hapi, by way of vision, has two methods for rendering views, h.view
and the view handler. Lets look at how to do both.First, rendering a view in Express:
app.get('/', function (req, res) {
res.render('index', { title: 'Homepage', message: 'Welcome' });
});
Using
h.view
in hapi:server.route({
method: 'GET',
path: '/',
handler: function (request, h) {
return h.view('index', { title: 'Homepage', message: 'Welcome' });
}
});
And using the view handler in hapi:
server.route({
method: 'GET',
path: '/',
handler: {
view: {
template: 'index',
context: {
title: 'Homepage',
message: 'Welcome'
}
}
}
});
To pass context in
h.view
, you pass an object as the second parameter. To pass context in the view handler, you use the context
key.express.static() -> inert
hapi gets its ability to serve static content from a plugin called inert. inert provides new handler methods for serving static files and directories, as well as adding a
h.file()
method to the response toolkit. For a more in-depth tutorial of server static files in hapi, please see the serving static files tutorial.Serving Single Files
In Express, you would use the
res.sendFile
method to return a single file. In hapi, you can either use the h.file()
method or the file handler, which is available via inert. Once you register the inert plugin, you will be able to serve your static files:
Express:
app.get('/image', function (req, res) {
res.sendFile('image.jpg', {root: './public'});
});
hapi with
h.file()
:const server = new Hapi.Server({
port: 3000,
routes: {
files: {
relativeTo: Path.join(__dirname, 'public')
}
}
});
await server.register(require('@hapi/inert');
server.route({
method: 'GET',
path: '/image',
handler: function (request, h) {
h.file('image.jpg');
}
});
hapi with file handler:
const server = new Hapi.Server({
port: 3000,
routes: {
files: {
relativeTo: Path.join(__dirname, 'public')
}
}
});
await server.register(require('@hapi/inert');
server.route({
method: 'GET',
path: '/image',
handler: {
file: 'image.jpg'
}
});
To serve static files in hapi, you first must tell hapi where the static files are located. You do this by configuring the
server.options.routes
object. You set the relativeTo
to the folder where the files are located, much like you do in the options object of the res.sendFile
in Express. Next, you need to register the inert plugin. This will give you access to the methods that allows you to serve static files. Now in your route handler, you can use the h.file()
method or the file handler to server your static file.Static File Server
To set up a static file server in Express, you would use the
express.static()
middleware. In hapi, you use the file handler made available by the inert plugin. You would setup the server in the same what as you did to server a single static file, by telling where the files are located. You then would setup a route to catch all of the requests and return the correct files. Lets have a look:
Express:
app.use(express.static('/public'))
hapi:
const server = new Hapi.Server({
port: 3000,
routes: {
files: {
relativeTo: Path.join(__dirname, 'public')
}
}
});
await server.register(require('@hapi/inert'));
server.route({
method: 'GET',
path: '/{param*}',
handler: {
directory: {
path: '.'
}
}
});
Now, you can access any static files by going to
localhost:3000/filename
. inert has many other options and capabilities. To see what all it can do, please see the serving static files tutorial.Error Handling -> boom
hapi uses the boom module to handle errors. By default, boom will return the errors in JSON format. Express on the other hand will return a text response by default, which is suboptimal with a JSON API. Lets look a 404 error response with the default settings by submitting a
GET
request to '/hello'
, which does not exists:
Express:
Cannot GET /hello
hapi:
{
"statusCode": 404,
"error": "Not Found",
"message": "Not Found"
}
Custom Messages
boom
allows you to easily change the error message for any status code. Lets take the 404 error above and return a new message:
Express:
res.status(400).send({status: 404, error: "Page not found"});
hapi:
Boom.notFound('Page not found');
In Express, you set the status code, then send the error message body. In this case we return a JSON object with the status code and the error message. In
boom
, there is no need to return a JSON object with the status code, it does this by default. In the example above, you call Boom.notFound()
to set the error message. boom
has a long list of 4xx and 5xx errors, such as Boom.unauthorized()
, Boom.badRequest()
, Boom.badImplementation()
, etc. For a complete list, please see the boomdocumentation.
No comments:
Post a Comment