How to make Express work more like hapi
5 minutes read
Like most people doing Node.js, I started building servers and APIs with Express. It feels minimal, fast and requires very little effort to start building. But then it starts requiring a lot of effort building features around it instead of building the actual business logic. That’s why I’ve since moved to hapi.
As the hapi website states:
hapi enables developers to focus on writing reusable application logic instead of spending time building infrastructure.
What this means is that hapi was built around the idea of configuration over code, with the right set of built-in functionality (cookies, cache, authentication, input validation, etc.) that can be configured and a powerful plugin system that allows you to very easily break your application up into isolated pieces of code.
Nowadays I use hapi to build web APIs but sometimes clients have already spent a lot of time and money in Express apps and the migration costs, for both people and time, may not seem like it’s worth the effort, so what I end up doing is trying to adapt some of the already built-in functionality I love from hapi into express.
In this article I’m going to show some of the easiest packages/techniques I’ve used to make Express work more like hapi. I assume you have a working knowledge of both frameworks.
Consider the given hapi plugin registration and route definitions:
1server.register(require('hapi-auth-basic'), (err) => {2 server.auth.strategy('simple', 'basic', {3 validateFunc: validate4 });5 server.route({6 method: 'GET',7 path: '/hello',8 config: {9 auth: 'simple',10 handler: function (request, reply) {11 reply('hello, ' + request.auth.credentials.name);12 }13 }14 });15});server.route({16 method: 'GET',17 path: '/hello/{name}',18 handler: function (request, reply) {19 reply('Hello ' + request.params.name + '!');20 },21 config: {22 validate: {23 params: {24 name: Joi.string().min(3).max(10)25 }26 }27 }28});
Here we are registering an authentication plugin plus two routes, one that uses the registered authentication and another that does not need authentication. This is a very basic example but it’ll lay the foundation for the remainder of the article.
Plugins
hapi’s powerful plugin system is one of it’s greatest strengths. It allows you to very easily break your application up into isolated pieces of business logic, and reusable utilities. Plugins can add routes, authentication strategies, register extension functions, among others.
Although most plugin-like functionality in Express can be added via middlewares, it can get messy pretty quickly. We can easily break an existing Express solution into several pieces by having files exporting a function that gets an Express app as the first parameter.
1// middleware.js2module.exports = (app) => {3 app.use(cors()); // Support for Cross-Origin Resource Sharing4 app.use(bodyParser.json()); // Support for Parsing JSON payload5 app.use(compression()); // Compress responses6};
Now you can just import it in your application main file:
1// index.js2const app = express();3require('./middleware')(app);4require('./database')(app);5require('./routes')(app);
This leads to cleaner and more modular code where everyone can collaborate. If you’re ever built anything with Express, you’ve probably done this already but I had to get it out there.
Edit: Lenny Martin has commented on a better approach by having each module exports an Express Router where you can mount middlewares or routes, which allows nesting of middlewares and routes, module reusability and composability. We can use it like this:
1// middlewares.js2const router = require('express').Router();router.use(cors());3router.use(bodyParser.json());4router.use(compression());module.exports = router;
And import it like:
1// index.js2const app = express();3app.use(require('./middlewares'));4app.use(require('./routes'));
Take a look at his response and example gist.
Caveats
The hapi plugin system offer other functionality harder to replicate on Express like error handling and managing dependencies between plugins.
Validation
hapi has great built-in validation with Joi for payload, path parameters and query strings. You can specify it in the route configuration, as seen in the first example in the config.validate object. If for some reason validation fails, the request won’t reach the route handler, so we can be pretty sure that when it does we can trust whatever data we read from it, which makes the handler code much smaller and precise.
This second topic is actually very easy to achieve in Express with the following package:
1npm i --save express-joi-validator
You now have route validations as a middleware:
1const validate = require('express-joi-validator');router.get('/hello/:name',2 validate({3 params: {4 name: Joi.string().min(3).max(10)5 }6 }),7 (req, res) => {8 res.send('Hello ' + req.params.name + '!');9 });
Pretty neat, right?! You can even pass a Joi options object as the second parameter for the validate function. If validation fails, the client gets a 400 status code and the request does not reach the route handler, which is very similar to how hapi works.
Caveats
Nothing to add here, which is great!
Authentication
Authentication within hapi is based on the concept of schemes and strategies. A scheme is a general type of authentication, like “basic”. A strategy is a pre-configured named instance of a scheme. In the initial example we’re using the basic authentication scheme and we’re registering a “simple” strategy. What it does exactly is not relevant here.
For Express we can use Passport, which is probably the best authentication middleware available. It can be integrated with a plethora of strategies so we just have to install the right package. For basic authentication, let’s do the following:
1npm i --save passport passport-http
Then in the code:
1const passport = require('passport');2const BasicStrategy = require('passport-http');passport.use(new BasicStrategy((username, password, done) => {3 // Some Basic authentication...4}));
Now the same route definition in Express would be something like:
1app.get('/hello',2 passport.authenticate('basic', { session: false }),3 (req, res) => {4 res.send('hello, ' + request.user.name);5 });
You can even have multiple pre-configured named strategies for the same scheme like you can have with hapi by using passport named strategies like this:
1passport.use('my-basic-authz', new BasicStrategy(...));
Caveats
What’s cool about hapi’s authentication strategies is that you can set a default authentication for all routes and then change or remove strategies for a specific route in it’s own configuration (i.e. configure all routes to use basic authentication, configure /login route to use no authentication). This is, to my knowledge, impossible to do with Express since you either register a middleware to authenticate all routes, or register the authentication middleware for each route where required. A somewhat simple way to get around it is to have a list of all routes that don’t require authentication and encapsulate the authentication middleware like this:
1const auth = function (opts) {2 return (req, res, next) => {3 if (opts.ignore.indexOf(req.path) !== -1) {4 // It belongs to the ignore list, move next.5 return next();6 } // Requires authentication, execute passport middleware.7 const middleware = passport.authenticate('basic', { session: false });8 middleware(req, res, next);9 };10};1112app.use(auth({ ignore: ['/login'] }));
Testing
Testing endpoints with hapi is a breeze with the server.inject function (although it can be used for other tasks as well), whether I’m using Mocha, Lab or any other testing framework. This is probably the thing I miss the most when working with other frameworks:
1const opts = {2 method: 'GET',3 url: '/hello',4 credentials: {5 name: 'André Jonas'6 }7}8server.inject(opts, (res) => {9 console.log(res.result);10 // Your assertions here...11});
those of you who are not familiar with the example above, we’re injecting a request (I say inject because Hapi is faking an incoming HTTP request and injecting it into the request pipeline) to the /hello
endpoint while also mocking the credentials object available in req.auth.credentials
. This allows us to test the endpoint without having to setup the database with users, log in the user, make the request with the authenticated user and all that jazz.
The closest I got to this with Express was with supertest, an http assertions library. Let’s install it with:
1npm i --save-dev supertest
Now we can do a similar request like:
1const app = require('./app.js');2const request = require('supertest');request(app)3 .get('/hello')4 .set('Authorization', 'Basic [YOUR CREDENTIALS HERE]')5 .expect(200)6 .end((err, res) => {7 console.log(res.result);8 // Your assertions here...9 });
Supertest can also do a nice thing for you, it can bound your server to an ephemeral port for you so you don’t need to keep track of ports.
Caveats
As you can see, we have to setup a valid authorization header that’ll go through the entire authentication flow. Although it’s nice to test this functionality, it can be very cumbersome to be forced to always set this up.
Conclusions
Both Express and hapi meet the same goal — building web APIs — but they try to accomplish it in different ways. As a matter of fact, hapi was originally built on top of express as Eran Hammer states in his blog back in 2012.
If you’re working on some express project and cannot make the jump to hapi, I really hope you can benefit from these easy tips. Use and abuse them! Depending on your reaction I might write a part II covering session management, security headers, cookie encryption and other cool hapi features we can adapt to Express.
Do you know a better way of doing any of these things in Express? Please, let me know! Happy coding!
Thanks José Nogueira and Pedro Teixeira for reviewing.