I spent more time than I should making this setup work, but I'm now finally able to  code without having to refresh the browser on every code change!

Here's the source code if you want to get straight into it.

Basic Express app with Nunjucks

// app.js

const express = require('express');
const path = require('path');
const bodyParser= require('body-parser');
const nunjucks = require('nunjucks');


const app = express();
const PORT_NUMBER = 3777;


app.set('views', path.join(__dirname, 'views'))
function configureNunjucks() {
	const env = nunjucks.configure(app.get('views'), {
  	autoescape: true,
  	noCache: true,
  	watch: true,
  	express: app
	})	
}
configureNunjucks();
app.set('view engine', 'html');

app.use(bodyParser.urlencoded({extended: true}));
app.use(express.static(path.join(__dirname, 'public')));

app.get('/', function (req, res) {
  res.render('index', {
    message: "It's working!"
  })
})

app.listen(PORT_NUMBER, function () {
  console.log('App is now listening on port ' + PORT_NUMBER);
})

Important note: For Nunjucks to work properly with BrowserSync, you need to turn off cache in Nunjucks. Note the noCache: true in the configureNunjucks function. Setting the noCache option to true will allow Nunjucks to recompile the template every time there are changes, which is what you want with browserSync!

Setting up Gulp with Nodemon and BrowserSync

// gulpfile.js

const { series, watch, parallel } = require('gulp');
const browserSync = require("browser-sync").create();
const nodemon = require("gulp-nodemon");

function startBrowserSync(){
  browserSync.init(null, {
	  
	  //proxy must match localhost port defined in app.js
      proxy: 'localhost:3777'
  });
}

function reloadHTML(){
  browserSync.reload();
}

// Watch all nunjucks files in views/ and all css and js in public/assets/, reload HTML if any changes. 
function watchFiles(){
  watch(['views/**/*.html','public/assets/**/*.*'], function(){
    reloadHTML();
  });
}


function startNodemon(cb){
  var callbackCalled = false;
    return nodemon({script: 'app.js'}).on('start', function () {
        if (!callbackCalled) {
            callbackCalled = true;
            cb();
        }
    });
}

//start nodemon and watchfile functions at the same time, while start browsersync immediately after nodemon.
exports.default = parallel(series(startNodemon, startBrowserSync),watchFiles);

Gulp is used to automate tasks in development workflow such as compiling/minifying css and javascript. In this case it's very useful to just start Nodemon and browserSync automatically every time you edit your code, which saves a lot of time refreshing the browser!

Note the above gulpfile syntax is for Gulp 4 onwards where each task is an asynchronous function and utilises series, parallel, watch and etc.

That's it!

The solution is fairly simple but it wasn't straightforward to figure out since a lot of the documentation comprises some combination but not all of the above. Especially when most examples are written prior to Gulp 4.

If you have any questions/suggestions just ping me on Twitter.