hapi.js, webpack and react-router with history api

Some months ago we presented you our port of the webpack-dev-server to hapi.js here. Requirements change and since we are always aiming on efficiency, we decided to overhaul our webpack + hapi.js approach and adapt to common development regarding the HTML5 history api. We eased our development setup and only need one hapi ui plugin, which either sends content from the dev server when in development mode or from the local final build when in production mode.

TL;DR We switched to a new dev setup with webpack-dev-server and hapi.js and kicked out our webpack-dev-plugin for hapi to gain the following advantages:

  • latest webpack-dev-server features
  • hot reloading
  • html5 history api support
  • lightweight environment agnostic ui plugin
  • streamlined development environment

We faced the problem, that the iframe mode can only be accessed via index.html, but if you leverage the power of the history api, the entry point could be every route, that is configured in the frontend router. It would take a lot of time and effort to refactor our webpack-dev-server plugin to support such features and even then a lot of other convenient functions that original dev server offers are still missing. Following this argumentation, there are 2 main points for the new setup:

  • we wanted a concise setup, which does not differ between development and production modes
  • we don’t want to reinvent the wheel and port all features from the original dev-server to our project, but still want to be able to use them

Now let’s come to the interesting part and give you insights to our new setup. As mentioned before, we are not using the webpack-dev-server plugin anymore, but have one ui plugin which is environment agnostic. Let’s set up a small Hapi server like so:

let server = new Hapi.Server();  
let conn = server.connection();

conn.register({  
    register: require("our ui module via relative path")
})

Since we are building a single page app with the innovative react.js framework, we just need one catch all route to deliver our html and all static assets bundled by webpack. That yields something like that:

module.exports = {};  
module.exports.register = function (server, options, next) {  
    var environment = process.env.NODE_ENV || 'development';

    //check if there is a route that ends with frontend/anyAsset.anyExtension
    var regex = new RegExp(".*\/frontend\/(.*?\..*)$");
    var tplArgs = {
        assetsOrigin: ""
    };

    if (environment == "development") {
        tplArgs.assetsOrigin = "https://localhost:8080/";
    }

    server.route([{
        method: 'GET',        
        path: '/{param*}', // catch all route which triggers when no other route matched before
        config: {
            auth: false
        },
        handler: function (request, reply) {
            var matches = regex.exec(request.path);
            //we found an asset based on our regex conventions (see above)
            if (matches) {
                //in dev mode we proxy to our running dev server
                if (environment == "development") {
                    return reply.proxy({
                        //ignore self signed cert errors
                        rejectUnauthorized: false,
                        mapUri: function (request, callback) {
                            callback(null, "https://localhost:8080/frontend/" + matches[1]);
                        }
                    });
                //in prod we send our compiled assets from disk
                } else {
                    return reply.file(process.cwd() + "/frontend/" + matches[1]);
                }
            //we send the main layout html
            } else {
                return reply.view('main.html', tplArgs);
            }
        }
    }]);

    next();
};

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

We check in which environment we are and respond from the local file system or proxy our request to the dev server. To be more understandable we hardcoded the dev-server url in this example, but it’s easy to make it configurable. To achieve this, we just use a so called catch-all-route which will always trigger, if there is no route with a higher specifity. Hapi will automatically order its routing table based on the specifity rules defined in the hapi.js docs. To sum it up, this means

When matching routes, string literals (no path parameter) have the highest priority, followed by mixed parameters (‘/a{p}b’), parameters (‘/{p}’), and then wildcard (/{p*}).

So, if our catch-all-route is triggered, we can be sure, that the client didn’t ask for a route processing data, but either for our main html layout or a static asset. The “magic” part now is the regex which checks if there is “frontend/somefile.extension” in the end of the request path. We defined for our app, that there is no frontend route with “frontend” in its name, so there won’t be any conflicts in routing. That means we can enter the website with every route defined in our frontend react-router without changing or adding additional routes in the backend. And with the setup above there are no changes in the webpack-dev-server necessary, too. That’s how a setup has to be: easy and maintainable.

Now let’s come to the second part: How did we wire everything together? Consider the following schema:

As you could see before in our ui hapi plugin, we request the entrypoint js in dev mode directly from the dev server (https://localhost:8080), so the web socket connection is established directly with the dev server. Since our asset dependencies are always require’d relatively, the client asks our backend for further assets (i.e. https://ourserver/frontend/somePic.png). This is why we need to proxy these requests in our ui plugin when in dev mode (Alternatively we could set up an absolute publicPath in the webpack config to the dev server, when in dev mode to prevent the need for the proxy routes). In production mode every call to these assets is served directly from disk, cause the plugin is environment agnostic and just switches automatically.

With this setup (production or development) you can enter your app with every frontend route (history api):

  • / (root)
  • /settings/myaccount
  • /thread/123

It will always serve your main html and assets correctly.

Webpack will include the inline bundle for controlling the hot code replacement in the bundle automatically (only in development mode). So with this setup everything works out of the box. Ok there is the need for a webpack-dev-server as new process in the background, but except from blocking an additional (but arbitrary) port, you can restart the hapi server without the need of recompiling your whole frontend what together with nodemon makes parallel api and frontend development a piece of cake.

You can further streamline the development startup by automatically spawning the webpack-dev-server process like so (Windows):

require("child_process").spawn("cmd", ["/c", "webpack-dev-server", "--https", "--inline", "--hot", "--contentBase", "path to your ui source"]  

One important caveat when using the https feature of the dev-server: You need to manually accept the self-signed certificate of the server or provide your own valid certificate.

Hope this saves you guys some time and we would love to hear your opinion on this.

Veröffentlicht in Blog