DEV Community

Cover image for How to build your own React boilerplate
Nick Karnik
Nick Karnik

Posted on • Edited on • Originally published at nick.karnik.io

How to build your own React boilerplate

What is a Boilerplate?

In programming, the term boilerplate code refers to blocks of code used over and over again.

Let’s assume your development stack consists of several libraries, such as React, Babel, Express, Jest, Webpack, etc. When you start a new project, you initialize all these libraries and configure them to work with each other.

With every new project that you start, you will be repeating yourself. You could also introduce inconsistencies in how these libraries are set up in each project. This can cause confusion when you switch between projects.

This is where boilerplates come in. A boilerplate is a template that you can clone and reuse for every project.

The modular Javascript ecosystem simplifies application development through various libraries, frameworks, and tools. Boilerplates can be daunting if you don’t understand the fundamentals of their underlying components. Let’s learn about these basic building blocks while creating our own.

Click here for source on GitHub

I am using Webstorm, Git, NodeJS v8.9, NPM v5.6 and React v16. Fire up your favorite IDE, create a blank project and let's get started!

Git Repository: Setup

Create a project folder and initialize a git repo:

mkdir react-boilerplate && cd react-boilerplate
git init
Enter fullscreen mode Exit fullscreen mode

You can connect this project to your own repo on GitHub using these instructions.

Readme File

Every project should contain a landing page with useful instructions for other developers. Let's create a README.md file under the project root with the following content:

# React-Boilerplate
This is my react-boilerplate

## Setup
npm install
npm run build
npm start
Enter fullscreen mode Exit fullscreen mode

GitHub displays the contents of the readme file on the landing page for the project

Now, commit the above changes to git:

git add .
git commit -m "created readme"
Enter fullscreen mode Exit fullscreen mode

At the end of each section, you should commit your code to git

Folder Structure

Create the following folder structure for your project:

react-boilerplate
    |--src
       |--client
       |--server
Enter fullscreen mode Exit fullscreen mode

with the command:

mkdir -p src/client src/server
Enter fullscreen mode Exit fullscreen mode

This folder structure is basic and will evolve as you integrate other libraries in the project.

Git Ignore

Once we build our project, there will be a few auto-generated files and folders. Let's tell git to ignore some of those files that we can think of ahead of time.

Create .gitignore under the root folder with the following content:

# Node
node_modules/

# Webstorm
.idea/

# Project
dist/
Enter fullscreen mode Exit fullscreen mode

Comments in a .gitignore file are prefixed with #

Node Package Manager

The starting point for a node project is to initialize its package manager which creates a file called package.json. This file must be checked into git.

It generally contains:

  • A description of your project for NPM
  • List of references to all installed packages
  • Custom command line scripts
  • Configuration for installed packages

Go to your project root and type the following:

npm init
Enter fullscreen mode Exit fullscreen mode

Fill out all the details and after you accept them, npm will create a package.json file that looks something like:

{
  "name": "react-boilerplate",
  "version": "1.0.0",
  "description": "Basic React Boilerplate",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/theoutlander/react-boilerplate.git"
  },
  "keywords": [
    "Node",
    "React"
  ],
  "author": "Nick Karnik",
  "license": "Apache-2.0",
  "bugs": {
    "url": "https://github.com/theoutlander/react-boilerplate/issues"
  },
  "homepage": "https://github.com/theoutlander/react-boilerplate#readme"
}
Enter fullscreen mode Exit fullscreen mode

Static Content

Let's create a static HTML file src/client/index.html with the following content:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>React Boilerplate</title>
</head>
<body>
    <div id="root">
        Welcome to React Boilerplate!
    </div>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

Express Web Server

To serve the static file above, we need to create a web server in ExpressJS.

NPM v5 automatically saves installed packages under the dependencies section in package.json so the --save attribute is not necessary

npm install express
Enter fullscreen mode Exit fullscreen mode

I would recommend following a file naming convention where file names are lower case and multiple words are separated by a dot. You will avoid running into case sensitivity issues across platforms as well as simplify naming files with multiple words across larger teams.

Create a file src/server/web.server.js and add the following code to host a web server via an express app and serve the static html file:

const express = require('express')

export default class WebServer {
  constructor () {
    this.app = express()
    this.app.use(express.static('dist/public'))
  }

  start () {
    return new Promise((resolve, reject) => {
      try {
        this.server = this.app.listen(3000, function () {
          resolve()
        })
      } catch (e) {
        console.error(e)
        reject(e)
      }
    })
  }

  stop () {
    return new Promise((resolve, reject) => {
      try {
        this.server.close(() => {
          resolve()
        })
      } catch (e) {
        console.error(e.message)
        reject(e)
      }
    })
  }
}
Enter fullscreen mode Exit fullscreen mode

We have created a simple web server above with a start and stop command.

Click here to learn more about Promises

Startup File

Next, we need to create an index file which will initialize various high-level components. In our example, we're going to initialize the web server. However, as your project grows you can also initialize other components such as configuration, database, logger, etc.

Create a file src/server/index.js with the following code:

import WebServer from './web.server'

let webServer = new WebServer();
webServer.start()
  .then(() => {
    console.log('Web server started!')
  })
  .catch(err => {
    console.error(err)
    console.error('Failed to start web server')
  });
Enter fullscreen mode Exit fullscreen mode

Babel

To run the above ES6 code, we need to transform it to ES5 first via Babel. Let's install Babel and the babel-preset-env dependency which supports ES2015 transpilation:

npm i babel-cli babel-preset-env --save-dev
Enter fullscreen mode Exit fullscreen mode

Create a babel configuration file called .babelrc under the root and add the following details to it:

{
  "presets": ["env"]
}
Enter fullscreen mode Exit fullscreen mode

The env preset implicitly include babel-preset-es2015, babel-preset-es2016, and babel-preset-es2017 together, which means you can run ES6, ES7 and ES8 code.

Build Commands

Let's create commands to build the server and client components of the project and start the server. Under the scripts section of package.json , remove the line with the test command and add the following:

"scripts": {
    "build": "npm run build-server && npm run build-client",
    "build-server": "babel src/server --out-dir ./dist",
    "build-client": "babel src/client --copy-files --out-dir ./dist/public",
    "start": "node ./dist/index.js"
}
Enter fullscreen mode Exit fullscreen mode

The build command above will create a dist/public folder under the root. The build-client command is simply copying the index.html file to the dist/public folder.

Starting up

You can run the babel transpiler on the code above and start the web server by using the following commands:

npm run build
npm start
Enter fullscreen mode Exit fullscreen mode

Open your browser and navigate to http://localhost:3000. You should see the output of your static HTML file.

React Boilerplate

You can stop the web server by pressing <Ctrl> C

Test Harness: Jest

I cannot stress enough the importance of introducing unit tests at the beginning of a project. We're going to use the Jest Testing Framework which is designed to be fast and developer friendly.

First, we need to install jest and save it to development dependencies.

npm i jest --save-dev
Enter fullscreen mode Exit fullscreen mode

Unit Tests

Let's add two test cases to start and stop the web server.

For test files, you should add a .test.js extension. Jest will scan the src folder for all files containing .test in the filename, you can keep your test cases under the same folder as the files they're testing.

Create a file called src/server/web.server.test.js and add the following code:

import WebServer from './web.server'

describe('Started', () => {
  let webServer = null

  beforeAll(() => {
    webServer = new WebServer()
  })

  test('should start and trigger a callback', async () => {
    let promise = webServer.start()
    await expect(promise).resolves.toBeUndefined()
  })

  test('should stop and trigger a callback', async () => {
    let promise = webServer.stop()
    await expect(promise).resolves.toBeUndefined()
  })
})
Enter fullscreen mode Exit fullscreen mode

Test Command

Let's add an npm command to run the test under the scripts section of package.json. By default, jest runs all files with the word .test in their file name. We want to limit it to running tests under the src folder.

"scripts": {
...
    "test": "jest ./src"
...
}
Enter fullscreen mode Exit fullscreen mode

babel-jest is automatically installed when installing Jest and will automatically transform files if a babel configuration exists in your project.

Let's run our tests via the following command:

npm test
Enter fullscreen mode Exit fullscreen mode

React Boilerplate

Our application is set up to serve a static HTML file via an Express web server. We have integrated Babel to enable ES6 and Jest for unit testing. Let's shift our focus to the front-end setup.

React Setup

Install the react and react-dom libraries:

npm i react react-dom
Enter fullscreen mode Exit fullscreen mode

Create a file called src/client/app.js with:

import React, {Component} from 'react'

export default class App extends Component {
    render() {
        return <div>Welcome to React Boilerplate App</div>
    }
}
Enter fullscreen mode Exit fullscreen mode

Let's render the App via an index file under src/client/index.js with:

import React from 'react'
import ReactDOM from 'react-dom'
import App from './app'

ReactDOM.render(<App />, document.getElementById('root'))
Enter fullscreen mode Exit fullscreen mode

Babel React

If you execute npm run build-client , you will get an error because we haven't told babel how to handle React / JSX.

React Boilerplate

Let's fix that by installing the babel-preset-react dependency:

npm install --save-dev babel-preset-react
Enter fullscreen mode Exit fullscreen mode

We also need to modify the .babelrc config file to enable transpiling react:

{
  "presets": ["env", "react"]
}
Enter fullscreen mode Exit fullscreen mode

Now, when you run npm run build-client , it will create app.js and index.js under dist/public with ES6 code transpiled to ES5.

Load Script in HTML

To connect our React App to the HTML file, we need load index.js in our index.html file. Don't forget to empty the text of the #root node since the React App will be mounted to it:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>React Boilerplate</title>
</head>
<body>
    <div id="root"></div>
    <script src="index.js"></script>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

Run Server

If you fire up your web server and go to http://localhost:3000, you will see a blank page with an error in the console.

Uncaught ReferenceError: require is not defined

React Boilerplate

This is because Babel is just a transpiler. In order to support dynamically loading modules, we will need to install webpack.

Start by changing the build commands under scripts in package.json to build-babel:

"scripts": {
    "build-babel": "npm run build-babel-server && npm run build-babel-client",
    "build-babel-server": "babel src/server --out-dir ./dist",
    "build-babel-client": "babel src/client --copy-files --out-dir ./dist/public",
    "start": "node ./dist/index.js",
    "test": "jest ./src"
  }
Enter fullscreen mode Exit fullscreen mode

Webpack

Webpack allows us to easily modularize our code and bundle it into a single javascript file. It is supported by numerous plugins and chances are that there's a plugin for almost any build task you can think of. Start by installing Webpack:

This tutorial was published right before webpack v4 was released, so we will explicitly install webpack v3.

npm i webpack@^3
Enter fullscreen mode Exit fullscreen mode

By default, webpack looks for a configuration file called webpack.config.js , so let's create it in the root folder and define two entry points, one for the web application and the other for the web server. Let's create two config objects and export them as a collection:

const client = {
    entry: {
        'client': './src/client/index.js'
    }
};

const server = {
    entry: {
        'server': './src/server/index.js'
    }
};

module.exports = [client, server];
Enter fullscreen mode Exit fullscreen mode

Now, let's specify where webpack will output the bundle and set the target build so that it ignores native node modules like 'fs' and 'path' from being bundled. For client, we will set it to web and for server we will set it to node.

let path = require('path');

const client = {
    entry: {
        'client': './src/client/index.js'
    },
    target: 'web',
    output: {
        filename: '[name].js',
        path: path.resolve(__dirname, 'dist/public')
    }
};

const server = {
    entry: {
        'server': './src/server/index.js'
    },
    target: 'node',
    output: {
        filename: '[name].js',
        path: path.resolve(__dirname, 'dist')
    }
};

module.exports = [client, server];
Enter fullscreen mode Exit fullscreen mode

Babel Loader

Before we can run webpack, we need configure it to handle ES6 and JSX code. This is done via loaders. Let's start by installing babel-loader:

npm install babel-loader --save-dev
Enter fullscreen mode Exit fullscreen mode

We need to modify the webpack configuration to include babel-loader to run on all .js files. We will create a shared object defining the module section that we can re-use for both targets.

const path = require('path');

const moduleObj = {
    loaders: [
        {
            test: /\.js$/,
            exclude: /node_modules/,
            loaders: ["babel-loader"],
        }
    ],
};

const client = {
    entry: {
        'client': './src/client/index.js',
    },
    target: 'web',
    output: {
        filename: '[name].js',
        path: path.resolve(__dirname, '/pub')
    },
    module: moduleObj
};

const server = {
    entry: {
        'server': './src/server/index.js'
    },
    target: 'node',
    output: {
        filename: '[name].js',
        path: path.resolve(__dirname, 'dist')
    },
    module: moduleObj
}

module.exports = [client, server];
Enter fullscreen mode Exit fullscreen mode

For merging nested shared object, I would recommend checking out the Webpack Merge module

Excluding Files

Webpack will bundle referenced libraries which means everything that is included from node_modules will be packaged. We don't need to bundle external code as those packages are generally minified and they will also increase the build time and size.

Let's configure webpack to exclude all packages under the node_modules folder. This is easily accomplished via the webpack-node-externals module:

npm i webpack-node-externals --save-dev
Enter fullscreen mode Exit fullscreen mode

Followed by configuring webpack.config.js to use it:

let path = require('path');
let nodeExternals = require('webpack-node-externals');

const moduleObj = {
    loaders: [
        {
            test: /\.js$/,
            exclude: /node_modules/,
            loaders: ["babel-loader"],
        }
    ],
};

const client = {
    entry: {
        'client': './src/client/index.js',
    },
    target: 'web',
    output: {
        filename: '[name].js',
        path: path.resolve(__dirname, 'dist/public')
    },
    module: moduleObj
};

const server = {
    entry: {
        'server': './src/server/index.js'
    },
    target: 'node',
    output: {
        filename: '[name].js',
        path: path.resolve(__dirname, 'dist')
    },
    module: moduleObj,
    externals: [nodeExternals()]
}

module.exports = [client, server];
Enter fullscreen mode Exit fullscreen mode

Update Build Command

Finally, we need to make changes to the scripts section under package.json to include a build command that uses webpack and to rename index.js to server.js for npm start as that's what webpack is configured to output.

"scripts": {
    "build": "webpack",
    "build-babel": "npm run build-babel-server && npm run build-babel-client",
    "build-babel-server": "babel src/server --out-dir ./dist",
    "build-babel-client": "babel src/client --copy-files --out-dir ./dist/public",
    "start": "node ./dist/server.js",
    "test": "jest ./src"
  }
Enter fullscreen mode Exit fullscreen mode

Build Clean

Let's add a command to clean our dist and node_modules folders so we can do a clean build and ensure our project still works as expected. Before we can do that, we need to install a package called rimraf (which is the rm -rf command).

npm install rimraf
Enter fullscreen mode Exit fullscreen mode

The scripts section should now contain

"scripts": {
...
"clean": "rimraf dist node_modules",
...
}
Enter fullscreen mode Exit fullscreen mode

Clean Build w/Webpack

You can now successfully clean and build your project using webpack:

npm run clean
npm install
npm run build
Enter fullscreen mode Exit fullscreen mode

This will create dist/server.js and dist/public/client.js under the root folder.

HTML Webpack Plugin

However, you may have noticed that index.html is missing. This is because previously we asked Babel to copy files that weren't transpiled. However, webpack isn't able to do that, so we need to use the HTML Webpack Plugin.

Let's install the HTML Webpack Plugin:

npm i html-webpack-plugin --save-dev
Enter fullscreen mode Exit fullscreen mode

We need to include the plugin at the top of the webpack config file:

const HtmlWebPackPlugin = require('html-webpack-plugin')
Enter fullscreen mode Exit fullscreen mode

Next, we need to add a plugins key to the client config:

const client = {
  entry: {
    'client': './src/client/index.js'
  },
  target: 'web',
  output: {
    filename: '[name].js',
    path: path.resolve(__dirname, 'dist/public')
  },
  module: moduleObj,
  plugins: [
    new HtmlWebPackPlugin({
      template: 'src/client/index.html'
    })
  ]
}
Enter fullscreen mode Exit fullscreen mode

Before we build the project, let's modify our HTML file and remove the reference to the index.js script because the above plugin will add that for us. This is especially useful when there are one or more files with dynamic filenames (for instance when files are generated with a unique timestamp for cache busting).

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>React Boilerplate</title>
</head>
<body>
    <div id="root"></div>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

Let's rebuild the project:

npm run clean
npm install
npm run build
Enter fullscreen mode Exit fullscreen mode

And, verify that our existing tests are still running:

npm test
Enter fullscreen mode Exit fullscreen mode

We have further updated the boilerplate to integrate React and Webpack, created additional NPM commands, dynamically referenced index.js in the HTML file, and exported it.

Enzyme Setup

Before we add a React test, we need to integrate Enzyme which will allow us to assert, manipulate and traverse react components.

Let's start by installing enzyme and enzyme-adapter-react-16 which is required to connect enzyme to a project using react v16 and above.

enzyme-adapter-react-16 has peer dependencies on react, react-dom, and react-test-renderer

npm i --save-dev enzyme enzyme-adapter-react-16 react-test-renderer
Enter fullscreen mode Exit fullscreen mode

Create a file src/enzyme.setup.js with the following content:

import Enzyme from 'enzyme'
import Adapter from 'enzyme-adapter-react-16'

Enzyme.configure({
    adapter: new Adapter()
})
Enter fullscreen mode Exit fullscreen mode

We need to configure jest to use src/enzyme.setup.js in package.json by adding the following section under the root object:

{
...
"jest": {
    "setupTestFrameworkScriptFile": "./src/enzyme.setup.js"
  }
...
}
Enter fullscreen mode Exit fullscreen mode

React Component Test

Let's test the App Component and ensure that it renders the expected text. In addition, we will take a snapshot of that component so we can ensure that its structure hasn't changed with every test run.

Click here to learn more about snapshot testing

Create a test case under src/client/app.test.js with the following content:

import App from './app'
import React from 'react'
import {shallow} from 'enzyme'

describe('App', () => {
  test('should match snapshot', () => {
    const wrapper = shallow(<App/>)

    expect(wrapper.find('div').text()).toBe('Welcome to React Boilerplate App')
    expect(wrapper).toMatchSnapshot()
  })
})
Enter fullscreen mode Exit fullscreen mode

If we run this test now, it will pass with a warning:

React Boilerplate

Let's fix that by installing a polyfill called raf:

npm i --saveDev raf
Enter fullscreen mode Exit fullscreen mode

And changing the jest configuration under package.json to:

{
...
"jest": {
    "setupTestFrameworkScriptFile": "./src/enzyme.setup.js",
    "setupFiles": ["raf/polyfill"]
  }
...
}
Enter fullscreen mode Exit fullscreen mode

Now, you can verify that all our tests are passing:

npm test
Enter fullscreen mode Exit fullscreen mode

React Boilerplate

After running the react test, you will notice a new file at src/client/snapshots/app.test.js.snap. It contains the serialized structure of our react component. It must be checked into git so it can be used to compare against the dynamically generated snapshot during a test run.

Final Run

Let's start the web server one more time and navigate to http://localhost:3000 to ensure everything works:

npm start
Enter fullscreen mode Exit fullscreen mode

React Boilerplate

I hope this article has given you insights into streamlining the process of starting a new project from scratch with Express | React | Jest | Webpack | Babel. It is a good idea to create your own reusable boilerplate so you understand what goes on under the hood and at the same time get a head-start when creating new projects.

We have barely scratched the surface and there is a lot of room for improvement to make this boilerplate production ready.

Here are some things for you to try:


You may also like


If this article was helpful, ❤️ it and follow me on Twitter.


Top comments (5)

Collapse
 
hjfitz profile image
Harry

Why the promise wrapper around Express? Is this so necessary?

Collapse
 
theoutlander profile image
Nick Karnik

Your observation is correct. It is not necessary. However, no matter how small your project, you should always focus on code quality and reusability.

Here are some of the advantages when using promises:

  1. It reduces the need for nested callbacks

  2. You can chain .then calls (after both .then and .catch) and pass values through each one of them

  3. You can pass the promise object around your code and use method chaining

  4. Most importantly, you can use the async-await keywords with ES7 to write cleaner code

Hope that helps.

Collapse
 
hjfitz profile image
Harry

I'm well aware of the benefits of promises - when used correctly they can really aid maintainability. Especially when used with async/await.

Everyone's used to normal express though -

const express = require('express');
const app = express();
app.listen(8080);

Wrapping something simple like this in a promise harms maintainability. It's something that a Dev inheriting a project just doesn't expect.

Thread Thread
 
theoutlander profile image
Nick Karnik

If the 'hello world' approach works for you, then, by all means, use it.

I would not make a broad assumption that "Everyone's used to normal express" with that code snippet.

It is not testable and doesn't provide a clean way to reliably perform any actions once the web server has started or ran into issues.

Any dev inheriting a project should not be making assumptions without reading the code.

If you think wrapping it in a promise harms maintainability, I would suggest taking courses on design patterns unless all you want to do is build toy projects.

Thread Thread
 
hjfitz profile image
Harry

I would not make a broad assumption that "Everyone's used to normal express" with that code snippet.

If you've set up express, you'll have used my example. Not some over-engineered promise wrapper.

If you're unfamiliar with express, their readme gives a very similar example, at the top. github.com/expressjs/express/blob/...

It is not testable
Setting up an app, like Express' readme states doesn't need to be. I'm confident that they test it.

Any dev inheriting a project should not be making assumptions
You are correct. But you also shouldn't be wrapping code in unnecessary promises.

If you think wrapping it in a promise harms maintainability
In this case, it does. A class for handling 3 lines of code is so unnecessary. The lines of .then() makes it more difficult to read. I'd actually suggest you took a class in writing maintainable code. I'm happy with the quality of my code, and my employers agree.