Update: @tmdpw pointed me to another connect middleware that is even easier to use. The code below has been adjusted accordingly.

I use BrowserSync in almost all of my web projects. It’s basically a development server on steroids that has a bunch of quality-of-life features, like live reloading (without needing a browser extension!), synchronizing the clients connected to it (e.g. syncing the scrolling and clicking across devices), and much, much more.

JavaScript routing for SPA’s will be usually be done in one of two ways: using hashes, or manipulating the URL using the History API. A hash url would look something like http://localhost:3000/#/dashboard, whereas a router that uses History will have cleaner URLs: http://localhost:3000/dashboard. I write most of my apps in React, using React Router if I need routing. It supports the History API with its browserHistory history that you tell React Router to use.

There’s one big catch with the clean URLs though. Let’s say we’re on the http://localhost:3000/dashboard route. If we refresh our browser, we’ll get an error saying the page can’t be found, instead of our SPA being displayed with the /dashboard route. This is because BrowserSync thinks you’re requesting the dashboard/ directory, and it can’t find an index.html file in that directory - because it doesn’t exist.

To fix this, we would have to make BrowserSync point all requests to our index.html page where our SPA is located, and its router will figure out the rest. Luckily, BrowserSync uses connect behind the scenes. In other words: it supports middleware. Thanks to the connect-history-api-fallback middleware, we can fix this behavior in a couple lines of code. Here’s an example where a BrowserSync server is started in a Gulp task:

import gulp from 'gulp';
import browserSync from 'browser-sync';
import historyFallback from 'connect-history-api-fallback';

gulp.task('serve', () => {
  browerSync.init({
    server: {
      baseDir: './static',
      middleware: [
        historyFallback()
      ]
    }
  });
});

That’s all there is to it! Now your SPA will load on all requests (e.g. http://localhost:3000/dashboard), and its router will determine what page to render.