History API Routing for SPAs using webpack-dev-server

When building a Single Page Application (SPA) using frameworks like React, Vue.js or Angular, you’ll eventually end up including routing within the application to allow navigating between the different views / pages of the application.

Most SPA-Routers support several modes of routing, e.g. hash-based routing of the form example.org/#/my/route (e.g. React Router’s HashRouter or Vue Router’s Default Mode). The advantage of hash-based routing is that it will work out of the box and does not require any extra settings on the server (or dev server) side.

However, most projects will opt for the history API based routing which allows for routes of the form example.org/my/route eventually (e.g. React Router’s BrowserRouter or Vue Router’s HTML5 History Mode) as these routes feel more natural and are easier to remember (and type) and are better for SEO. These advantages come at the cost of configuring the webserver correctly, since the webserver needs to know that it should not handle routes like example.org/my/route but instead delegate the interpretation of this route to the SPA.

Development

Most SPAs these days leverage module bundlers like webpack, rollup.js or parcel and their companion development servers to serve (and auto reload) the page during development. To ensure these development servers delegate the route handling to the SPA, two important settings need to be configured (and they are similar for webpack’s DevServer, rollup.js’s serve plugin):

  1. We need to enable the so-called History API Fallback which basically means the server should fallback to serve index.html in case the requested route cannot be served directly by the webserver (i.e. if the path does not exist). This solves the issue of delegating routing to the SPA. However, we also need
  2. to ensure that the path to the SPA’s bundled javascript file is set to /bundle.js and not just bundle.js, otherwise the browser will fail to load the bundle on nested routes like /my/route as it will search for the bundle.js relative to the current “folder”, i.e. /my/bundle.js which does not exist. This problem can easily be mitigated for webpack by defining the publicPath variable as seen below:

webpack.config.js

module.exports = {
    // ...
    output: {
        // ...
        publicPath: "/",
        // ...
    },
    devServer: {
        // ...
        historyApiFallback: true,
        // ...
    },
    // ...
};

All credits go to Bing Lu for figuring this out in his response on stackoverflow and the webpack-dev-server github community

Hint: if you’re using browsersync, check out this excellent blog article on how to get the History API Fallback setup.

Production

In production, your SPA will not be served by a development server anymore, so you’ll need to take care that your web- or application server supports History-based routing. The setup will depend on your tech stack and will most likely just work for simple web servers like nginx without further configuration but if you’re relying on a HTTP server like connect or derivatives like express, then you might find the [connect-history-api-fallback] middleware helpful.