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
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
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"
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
with the command:
mkdir -p src/client src/server
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/
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
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"
}
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>
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
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)
}
})
}
}
We have created a simple web server above with a start and stop command.
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')
});
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
Create a babel configuration file called .babelrc under the root and add the following details to it:
{
"presets": ["env"]
}
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"
}
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
Open your browser and navigate to http://localhost:3000. You should see the output of your static HTML file.
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
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()
})
})
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"
...
}
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
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
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>
}
}
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'))
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.
Let's fix that by installing the babel-preset-react dependency:
npm install --save-dev babel-preset-react
We also need to modify the .babelrc config file to enable transpiling react:
{
"presets": ["env", "react"]
}
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>
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
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"
}
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:
npm i webpack
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];
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];
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
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];
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
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];
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"
}
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
The scripts section should now contain
"scripts": {
...
"clean": "rimraf dist node_modules",
...
}
Clean Build w/Webpack
You can now successfully clean and build your project using webpack:
npm run clean
npm install
npm run build
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
We need to include the plugin at the top of the webpack config file:
const HtmlWebPackPlugin = require('html-webpack-plugin')
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'
})
]
}
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>
Let's rebuild the project:
npm run clean
npm install
npm run build
And, verify that our existing tests are still running:
npm test
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
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()
})
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"
}
...
}
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.
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()
})
})
If we run this test now, it will pass with a warning:
Let's fix that by installing a polyfill called raf:
npm i --saveDev raf
And changing the jest configuration under package.json to:
{
...
"jest": {
"setupTestFrameworkScriptFile": "./src/enzyme.setup.js",
"setupFiles": ["raf/polyfill"]
}
...
}
Now, you can verify that all our tests are passing:
npm test
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
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:
- Enable cache busting in Webpack
- CSS file bundling using css-loader in webpack
- Enable source maps in webpack
- Add debug commands to package.json
- Hot module replacement
- Auto-restart web server when changes are detected via nodemon
If you would like to learn more about the react ecosystem, I would highly recommend taking The Complete React Web Developer Course by Andrew Mead.
Great article, thank you very much! What about debugging ES6 server side code?
You’re welcome @crushjz! Debugging ES6 code is configured in webpack via the devtool attribute as listed here: https://webpack.js.org/configuration/devtool/.