Chances are you've heard of Hapi by now. And you might be wondering how it compares to the Express web framework in Node.js development. In this article, we will compare the frameworks head-to-head and explore the differences in experience for the developer.

Similarities and Differences Between Hapi and Express

Both Express and Hapi aim to be highly flexible, simple, and extensible. This similarity means that both have easy-to-use APIs, they're highly modular, and can support your application as it grows potentially very large.

The learning curve for these frameworks, since they are quite straightforward, is low, unlike a more opinionated framework like Meteor. If you are coming from using Express you should be able to quickly pick up Hapi, and vice versa.

There are, however, some philosophical differences between the two frameworks, which we'll describe throughout this article.

Hapi.js includes more "batteries" by default than Express does. For instance, when parsing payload from forms to via "POST" requests, with Express, you have to include the body-parser middleware. You then use that to parse the POST payload and use the data from the user. On the other hand, with Hapi, you need no middleware. The payload is parsed for you by the framework itself, and you can access the payload directly on the request object. Little conveniences like that are replete in Hapi.

Basic Server and Routing

To get started we will create hapiapp, an example Hapi app that shows basic Hapi functionality. We will also create expressapp, a mirror image of hapiapp that uses Express so we can compare the frameworks side by side.

Both of our apps will use ES6 JavaScript.

Create two directories, hapiapp and expressapp.

Inside each, run the command:

$ npm init

Then accept all the defaults by pressing "Enter". This creates a package.json file inside each directory, thus creating our two different apps, hapiapp and expressapp.

First, let's see basic routing in action in Hapi.js. Inside the hapiapp directory, install the Hapi module by running the following command:

$ npm install hapi@16.4.3 --save

Then create a file index.js with the following contents:

// hapiapp/index.js const Hapi = require('hapi'); // create our server
const server = new Hapi.Server(); // configure the port
server.connection({ port: 8000, host: 'localhost'
}); // basic routes
server.route({ method: 'GET', path: '/', handler: (request, reply) => reply('Hello World')
}); server.route({ method: 'GET', path: '/hello/{name}', handler: (request, reply) => reply(Hello ${})
}); // start the server
server.start((err) => { if (err) { console.error(err); } console.log('App running on port 8000...');

This basic app creates two routes at localhost:8000 and localhost:8000/hello/<name>. The first one will print out a simple "Hello World" to the user. The second, however, is a dynamic route that prints out "Hello" followed by the name we input in the browser after the second "/" slash.

From the hapiapp directory run the app using the following command and try out the URLs given above:

$ node index.js

The equivalent Express app involves a very similar setup. Inside our expressapp directory, install Express as a dependency using the command:

$ npm install express@4.16.2 --save

Then create a file index.js with the following contents:

// expressapp/index.js const express = require('express'); // create our app
const app = express(); // basic routes
app.get('/', (req, res) => res.send('Hello World!')); app.get('/hello/:name', (req, res) => res.send(Hello ${})); // start the server
app.listen(3000, () => console.log('App running on port 3000...')); 

This accomplishes the same functionality as the Hapi app. To run it, from inside our expressapp directory, run the command:

$ node index.js

Visit the routes given above, except now we are running at localhost:3000 instead of localhost:8000.

Both code examples have a straightforward simplicity. The server setup is remarkably similar, though Express looks more compact here.

Working with Middleware

"Middleware" is the name given to software modules that work on HTTP requests in succession before the final output is returned to the user in a response.

Middleware can work as functions which we define inside the application, or functions defined in third-party middleware libraries.

Express lets you attach middleware to handle each request. Hapi, however, works with plugins that provide middleware functionality.

An example of middleware is CSRF tokens, which help prevent Cross-Site Request Forgery (CSRF) hacking attacks. Without CSRF tokens, attackers can impersonate legitimate requests and steal data or run malicious code on your applications.

The approaches taken by Hapi and Express to deal with CSRF attacks is similar. It involves generating a secret token on the server for each form that submits data back to the server. Then the server checks each POST request for the correct, difficult-to-forge token. If it is absent, the server rejects the request, thus avoiding malicious code execution attempts. In this case the middleware will generate a CSRF token for each request so you don't have to do it within your application code.

The difference between the two frameworks here is mainly cosmetic, with Hapi using a plugin, Crumb, for the token generation and processing. Express, on the other hand, uses a middleware known as csurf to generate and process CSRF tokens.

Here is a Hapi code example generating Crumb tokens:

'use strict'; const Hapi = require('hapi'); const Vision = require('vision'); const server = new Hapi.Server({ host: '', port: 8000
}); const plugins = [ Vision, { plugin: require('../'), options: { restful: true } }
]; // Add Crumb plugin
(async () => { await server.register(plugins); server.route([ // a "crumb" cookie should be set with any request // for cross-origin requests, set CORS "credentials" to true // a route returning the crumb can be created like this { method: 'GET', path: '/generate', handler: function (request, h) { return { crumb: server.plugins.crumb.generate(request, h) }; } }, // request header "X-CSRF-Token" with crumb value must be set in request for this route { method: 'PUT', path: '/crumbed', handler: function (request, h) { return 'Crumb route'; } } ]); await server.start(); console.log('Example restful server running at:',;

And the roughly equivalent Express code using csurf.

const cookieParser = require('cookie-parser') const csrf = require('csurf') const bodyParser = require('body-parser') const express = require('express') // setup route middlewares
let csrfProtection = csrf({ cookie: true }) let parseForm = bodyParser.urlencoded({ extended: false }) // create express app
let app = express() // parse cookies
// we need this because "cookie" is true in csrfProtection
app.use(cookieParser()) app.get('/form', csrfProtection, function (req, res) { // pass the csrfToken to the view res.render('send', { csrfToken: req.csrfToken() })
});'/process', parseForm, csrfProtection, function (req, res) { res.send('data is being processed')

Middleware libraries can also help you with implementations of other common functionality such as authentication. You can find a list of some of the Express middleware at this middleware directory. For Hapi, there is a comparable library of Hapi plugins.

Serving Images and Static Assets

Serving static files is remarkably similar between Hapi and Express. Hapi uses the Inert plugin, whereas Express uses the express.static built-in middleware.

In our hapiapp application, we can serve static files from a public directory by creating a directory at hapiapp/public and the placing in it a file runsagainstbulls.jpg.

Here is the image file. Right click and download it into your application.

Example image

You'll also want to install inert@4.2.1 using:

$ npm install inert@4.2.1 --save

Now we can serve the static file using the following code. Add this before the lines that start the server in hapiapp/index.js.

// hapiapp/index.js ... // serve static files using inert
// require inert for this route
server.register(require('inert'), (err) => { if (err) { throw err; } // serve the runswithbulls image from the public folder server.route({ method: 'GET', path: '/image', handler: (request, reply) => { reply.file('./public/runsagainstbulls.jpg'); } });

Run the server again and visit localhost:8000/image and you should see the image served back to you.

Serving static files

In Express, there is a bit less setup. Just copy the same image to a newly-created public folder inside expressapp. Then add this line to your index.js file before the line that starts the server:

// expressapp/index.js ... // serve static files from the public folder

Start your server and visit localhost:3000/runsagainstbulls.jpg and you should see the image being served by Express.

Serving static in Express

By default Express will serve all files in the configured folder, and Hapi can be configured to do the same with Inert, although it is not the default setting.

Using Template Engines

Both Hapi and Express can render and serve templates like using engines like Handlebars, Twig, EJS and others.

In our hapiapp folder, install the vision plugin for rendering support with:

$ npm install vision@4.1.1 --save

Also install Handlebars template engine with:

$ npm install handlebars@4.0.10 --save

Then let's create a Handlebars template in a new directory templates, and a new file in that directory, named cats.html. We're going to make a listing of cats, and display them using this template.

Update cats.html as follows:

<!-- hapiapp/templates/cats.html --> <h2>All My Cats</h2> <ol> {{#each cats}} <li>{{name}}</li> {{/each}}

This Handlebars template loops through a collection of cat objects, and renders the name of each cat.

Inside index.js, before starting the server, add the following code, which configures vision and creates a route at /cats, and supplies a list of cats to be displayed there.

// hapiapp/index.js ... // configure vision to render Handlebars templates
server.register(require('vision'), (err) => { if (err) { throw err; } server.views({ engines: { html: require('handlebars'), }, path: __dirname + '/templates' });
}); // display cats at /cats using the cats handlebars template
server.route({ method: 'GET', path: '/cats', handler: (request, reply) => { reply.view('cats', { cats: [ {name: "Blinky the Cat"}, {name: "Sammy the Happy Cat"}, {name: "Eto the Thug Cat"}, {name: "Liz a quiet cat"}, ] }); }

Start the server again and visit localhost:8000/cats, and you should see a list of cats displayed on the page.

List of Cats

To replicate this functionality in Express, first create a directory named views inside the expressapp root directory. Then create a Handlebars file cats.hbs with contents similar to what we had for Hapi.

<!-- expressapp/views/cats.hbs --> <h2>All My Cats</h2> <ol> {{#each cats}} <li>{{name}}</li> {{/each}}

Now, back in expressapp install the Handlebars engine for Express with:

$ npm install hbs@4.0.1 --save

Then to render the template and display our cats, update index.js, adding the following code just before the part that starts the server:

// expressapp/index.js ... // use handlebars as the view engine
app.set('view engine', 'hbs'); // display a list of cats at /cats
app.get('/cats', function (req, res) { res.render('cats', { cats: [ {name: "Blinky the Cat"}, {name: "Sammy the Happy Cat"}, {name: "Eto the Thug Cat"}, {name: "Liz a quiet cat"}, ] });

Restart the Express server, and visit localhost:3000/cats, and we should see the list of cats displayed as it was for Hapi.

You will notice that the Hapi example needed a bit more configuration than the Express example. Both code examples are quite simple to follow, however. In a small app like this one, there is little to choose between the two frameworks. In a larger application, however, there will be more noticeable differences. For example, Hapi has a reputation for involving more boilerplate code than Express. On the other hand, it also does a bit more for you out of the box than Express, so there are definitely trade-offs.

Connecting to a Database

MongoDB is a battle-tested NoSQL database to use in your applications. With the mongoose object-modeling library, we can connect MongoDB to both Hapi and Express apps. For this part, you will want to have MongoDB installed and running on your system if you don't already have it.

To see how this works for both frameworks, let us now update our sample applications to store cat objects in a database. Likewise, our listings will fetch cat objects from the database.

The steps we have to follow are:

  • Set up a MongoDB connection to localhost

  • Define a mongoose schema

  • Create objects

  • Store objects

  • Retrieve documents from the database

Back in our hapiapp directory, install mongoose with:

$ npm install mongoose@4.11.1 --save

Since we want the ability to create new cats from our /cats view, update the cats template to include a form for new cats. Add the following lines above the list of cats in hapiapp/templates/cats.html.

<!-- hapiapp/templates/cats.html --> ... <form class="" action="/cats" method="post"> <input type="text" name="name" placeholder="New cat">

We can now import mongoose at the top of our index.js file. Just below the line where we import Hapi, add the following lines:

// hapiapp/index.js ... const Hapi = require('hapi'); const mongoose = require('mongoose'); // connect to MongoDB
mongoose.connect('mongodb://localhost/hapiappdb', { useMongoClient: true }) .then(() => console.log('MongoDB connected')) .catch(err => console.error(err)); // create a Cat model that can be persisted to the database
const Cat = mongoose.model('Cat', {name: String}); 

Now update our "GET" route for /cats to fetch cats dynamically from MongoDB. We will also add a new "POST" route where we can save cats to the database. Here is the code to make these changes, replacing the old GET /cats code:

// hapiapp/index.js ... // display cats at /cats using the cats handlebars template
server.route({ method: 'GET', path: '/cats', handler: (request, reply) => { let cats = Cat.find((err, cats) => { console.log(cats); reply.view('cats', {cats: cats}); }); }
}); // post cats
server.route({ method: 'POST', path: '/cats', handler: (request, reply) => { let catname =; let newCat = new Cat({name: catname});, cat) => { if (err) { console.error(err); } return reply.redirect().location('cats'); }); }

Now when you run the server again and visit localhost:8000/cats, you will see that the cat list is now empty. But there is a form where you can enter new cat names and press the Enter key, and your cats will be saved to MongoDB. Enter a few cats and you get a list of cats displayed, as shown below:

New cat listings

For Express, installing and setting up Mongoose will look almost the same. Back in expressapp, install Mongoose with:

$ npm install mongoose@4.11.1 --save

Then add the Mongoose connection code and Cat model in index.js after importing Express.

// expressapp/index.js ... const express = require('express'); const mongoose = require('mongoose'); // connect to MongoDB
mongoose.connect('mongodb://localhost/expressappdb', { useMongoClient: true }) .then(() => console.log('MongoDB connected')) .catch(err => console.error(err)); // create a Cat model that can be persisted to the database
const Cat = mongoose.model('Cat', {name: String}); 

Notice the similarity to what we did for Hapi.

Now update our expressapp/views/cats.hbs template with the form code.

<!-- expressapp/views/cats.hbs --> ... <form class="" action="/cats" method="post"> <input type="text" name="name" placeholder="New cat">

To enable Express to parse the form contents, we will need to import the body-parser middleware. After the line importing Express in index.js, import body-parser as follows:

// expressapp/index.js ... const express = require('express'); const bodyParser = require('body-parser') 

In addition, activate the middleware after the line creating our Express app, as follows:

// expressapp/index.js ... // create our app
const app = express(); // parse application/x-www-form-urlencoded
app.use(bodyParser.urlencoded({ extended: false })); 

Now update our "GET" path for /cats and create a new "POST" route that create cats in the database:

// expressapp/index.js ... // display a list of cats at /cats
app.get('/cats', function (req, res) { let cats = Cat.find((err, cats) => { console.log(cats); res.render('cats', {cats: cats}); });
}); // post new cats and save them in the database'/cats', function (req, res) { console.log(; let catname =; let newCat = new Cat({name: catname});, cat) => { if (err) { console.error(err); } console.log(Cat ${} saved to database.) res.redirect('/cats'); });

Now, restart the server and visit localhost:3000/cats. Just like for the Hapi app, we will now see an empty space with a form on top. We can enter cat names. The view will refresh, showing us a list of cats now saved to our MongoDB database.

This is one instance where the Express set up was a bit more complex than the equivalent for Hapi. However, notice that we didn't actually need to do too much with Hapi or Express to get the database connection set up - most of the work here was for the GET and POST routes.

Choosing Between Hapi and Express

As we saw, Hapi and Express have a lot in common. Both have an active user base, with Hapi being widely adopted by big enterprise teams. However, Express continues to outpace most other frameworks in adoption, especially for smaller teams.

Hapi makes sense if you have a well-defined app idea that fits within sensible defaults for the programming approach. Hapi takes a particular approach to things like app security, and its plugins system is not as extensive as the Express middleware ecosystem. If you are a large team, the conventions of Hapi can come in useful to keep your code maintainable.

Express, on the other hand, works best if you are after flexibility and making most development decisions on your own. For example, there might be ten different middleware that do the same thing in the Express ecosystem. It's up to you to consider from many options and make your choice. Express is best if you have an app you are not sure about the exact direction it will take. Whatever contingencies arise, the large Express ecosystem will provide the tools to make it happen.

Learn More

Want to learn more about web development in Node.js and actually create some Express.js apps? Here are a few courses that will give you some great experience:

Otherwise you can also get a great start by visiting each framework's respective websites:

Either way, the most important thing is you actually try them out, work through some examples, and decide on your own which is best for you.