Express+React Monorepo Setup with Lerna
Tags: javascript, express, react, monorepo
Published: 2019-08-31 20:44:58

---
title: Express+React Monorepo Setup with Lerna
published: true
description: Setup an Express+React monorepo with Git pre-commit hooks, ESLint and Jest
tags: javascript,express,react,monorepo
canonical_url: https://benoithubert.net/2019/08/express-react-monorepo-setup-with-lerna.html
crossposted: true
---

_Initially published on my [blog](https://benoithubert.net/2019/08/express-react-monorepo-setup-with-lerna.html)_

**Changelog**
* 2019-08-31: added a 5th step (backend-frontend connection, serve React build from Express)

## Motivation

Setting up a basic full-stack JavaScript application is not that hard by itself, but becomes complicated and tedious as you throw in more requirements, such as performing linting and testing before allowing commits.

I've been investigating ways to do it properly, out of personal interest, and with the aim of teaching good practices to my students. Enforcing strict coding conventions tends to annoy them at first, but since we do it at an early stage of their training, it quickly becomes natural for them to follow good practices.

In this post, we'll describe how to set up an Express + React application repository. First, let's describe our requirements.

## Requirements

**We'll setup a monorepo**, using [Lerna](https://lerna.js.org/). As the name implies, in a monorepo, you keep all your app's "components" in a single repository. Lerna refers to these components as "packages". Among other things, it allows you to run `npm` scripts in all the packages with a single command, for tasks such as:

  * starting your app (`npm start`),
  * running tests (`npm test`),
  * or any custom script

**In order to improve code quality**, and prevent anyone from pushing broken code to GitHub, we'll setup Git pre-commit hooks: [Git hooks](https://githooks.com/) allow you to automatically perform tasks on specific Git events (pre-commit, pre-push, etc.). We'll set them up using [Husky](https://github.com/typicode/husky), in order to perform these tasks on pre-commit events:

* Linting with ESLint (Airbnb coding style)
* Testing with Jest

**Additionally**, we'll set up the backend package in order to use ES6 modules, and use [Yarn](https://yarnpkg.com/) for dependency management.

## Steps

We'll break down the following into 5 major steps:
1. Repo initialization and Lerna setup
2. Frontend app setup, with ESLint/Airbnb config
3. Backend app setup, with ESLint/Airbnb config
4. Git pre-commit hooks setup with Husky
5. Connect frontend and backend apps

### Repository initialization

This part is quite straightforward.

* Install Yarn globally if it's not already done: `npm i -g yarn`
* Create an empty directory and `cd` into it
* Initialize a Git repo: `git init`
* Initialize root-level `package.json`: `yarn init --yes` (modify `version` to `0.0.1` afterwards)
* Install Lerna and Husky as a dev dependency, at repo root level: `yarn add --dev lerna`
* Create Lerna config: `npx lerna init`, modify the version, and add `"npmClient": "yarn"` to the generated `lerna.json`
* Create a global `.gitignore`
* Write a minimal `README.md`

Here's the content of the initial `.gitignore`:

```
node_modules
.DS_Store
```

And the `lerna.json` file:

```json
{
  "npmClient": "yarn",
  "packages": [
    "packages/*"
  ],
  "version": "0.0.1"
}
```

Let's commit that before moving on! You can review this first commit [here](https://github.com/bhubr/express-react-monorepo/commit/d2e063a8353e600b3673bc3d38e81bfb0bfe69d0).

### Frontend app setup with CRA

We're gonna use [Create React App](https://create-react-app.dev/) to bootstrap the frontend app. You need to install it first: `npm i -g create-react-app`.

Before getting any further, let's create a branch. We're doing this in order to break down the steps into digestible bits, but will squash-merge intermediate branches at the end of each major step.

```sh
git checkout -b setup-frontend
```

Then let's generate the frontend app:

```sh
cd packages
create-react-app front
```

Then remove useless some files from `front/src` that we won't use:

```sh
cd front
rm README.md src/index.css src/App.css src/logo.svg
```

We have to remove the imports from `index.js` and `App.js` accordingly, and we'll replace the JSX returned by `App` with a simple "Hello World".

Let's check that the app works, `git add` everything and commit after that! Not of much interest since it's mostly auto-generated stuff, but you can review this commit [here](https://github.com/bhubr/express-react-monorepo/commit/d2e063a8353e600b3673bc3d38e81bfb0bfe69d0).

#### Custom ESLint setup

CRA provides a default ESLint setup. It's under the `eslintConfig` key of `package.json`:

```
  ...
  "eslintConfig": {
    "extends": "react-app"
  },
  ...
```

We're gonna change this config, in order to use Airbnb's coding style.

We first initialize a stand-alone ESLint config file:

```sh
npx eslint --init
```

Then we setup ESLint with Airbnb coding style, with the following choices:
* How would you like to use ESLint? **To check syntax, find problems, and enforce code style**
* What type of modules does your project use? **JavaScript modules (import/export)**
* Which framework does your project use? **React**
* Where does your code run? **Browser**
* How would you like to define a style for your project? **Use a popular style guide**
* Which style guide do you want to follow? **Airbnb (https://github.com/airbnb/javascript)**
* What format do you want your config file to be in? **JSON**
* Would you like to install them now with npm? (Y/n) **N** (we'll install them with Yarn)

After that we can:
* copy-paste generated `.eslintrc.json`'s content to under the `eslintConfig` section of `package.json` (that's why we chose JSON),
* delete `.eslintrc.json` to avoid redundancy,
* install the deps with Yarn: `yarn add --dev eslint@^6.2.2 typescript@latest eslint-plugin-react@^7.14.3 eslint-config-airbnb@latest eslint-plugin-import@^2.18.2 eslint-plugin-jsx-a11y@^6.2.3 eslint-plugin-react-hooks@^1.7.0`,
* test the config with `npx eslint src/`, which reports many errors - most of them due to the `src/serviceWorker.js` file,
* create a `.eslintignore` file to ignore the `src/serviceWorker.js` file (which we won't modify anyway),
* re-run `npx eslint src/`, which complains about JSX in `.js` files, and `it` being not defined (in `App.test.js`),
* rename the `.js` files to give them the `.jsx` extension:

    * `cd src`
    * `git mv App.js App.jsx`
    * `git mv App.test.js App.test.jsx`
    * `git mv index.js index.jsx`
* run the linter again - getting a weird `All files matched by 'src' are ignored.` message, which we can fix by running ESLint with `npx eslint src/\*\*/\*.js\*`,
* fix the `'it' is not defined` error by adding `"jest": true` to `env` section in `eslintConfig`,
* add `"lint": "npx eslint --fix src/\*\*/\*.js\*",` under the `scripts` key

After that, we can lint our frontend app by simply running `yarn lint`.

Let's stage and commit that! Find this commit [here](https://github.com/bhubr/express-react-monorepo/commit/0c719059f9828f1deb77c6753b5c83ae90288fe3).

After that, let's squash-merge the `front-setup` branch into `master` - done via [this PR](https://github.com/bhubr/express-react-monorepo/pull/1).

### Backend app setup

This step is gonna be a bit more complicated, so again, we're gonna create an intermediate branch, in order to break it down (after having pulled our `master` branch).

```sh
git checkout -b setup-backend
```

#### Simple server creation

Get back to the `~/packages` folder, then:

```sh
mkdir -p back/src
cd back
npm init --yes
yarn add express body-parser
```

Let's edit `package.json` and set `version` to `0.0.1`, and `main` to `build/index.js`, before we move on.

Let's also create a `.gitignore` files to ignore `node_modules`. That's redundant with the root `.gitignore` file, but could be useful if we take out the `back` package out of this repo, for stand-alone use. Besides, we'll have specific stuff to ignore on the backend side.

We're gonna create a simple server in `src/index.js`, using ES6 import/export syntax:

```javascript
// src/index.js
import express from 'express';
import bodyParser from 'body-parser';

const port = process.env.PORT || 5000;
const app = express();

app.listen(port, (err) => {
  if (err) {
    console.error(`ERROR: ${err.message}`);
  } else {
    console.log(`Listening on port ${port}`);
  }
});

```

Of course, unless we use Node 12 with `--experimental-modules` flag, running `node src/index` fails with:

    import express from 'express';
           ^^^^^^^

    SyntaxError: Unexpected identifier
        at Module._compile (internal/modules/cjs/loader.js:723:23)
        ...

I'm not comfortable with using experimental stuff in production, so Babel still seems a more robust option. We'll set it up before committing anything.

#### Babel setup

Sources:
* [@babel/node](https://babeljs.io/docs/en/next/babel-node.html)
* [Example Node Server with Babel](https://github.com/babel/example-node-server)

Let's install all we need: Babel, and also nodemon to restart our server on every change.

```sh
yarn add --dev @babel/cli @babel/core @babel/preset-env @babel/node nodemon
```

`@babel/node` will allow us to run ES6 code containing `import` and `export` statements. The doc explicitly advises not to use it in production, but the other Babel tools will allow us to generate a build suitable for production use.

Then create a `.babelrc` file containing this:

```json
{
  "presets": ["@babel/preset-env"]
}
```

Then add a `start` script to `package.json`:

```
  ...
  "scripts": {
    "start": "nodemon --exec ./node_modules/@babel/node/bin/babel-node.js src/index",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  ...
```

Now we can start our server using `yarn start`. Hurray! Let's stage and commit our whole `back` folder (find the commit [here](https://github.com/bhubr/express-react-monorepo/commit/f54053b91b4a0ef47ba762af74d212a6be88144f)).

#### Build setup

We'll store the production build in the `build` folder inside `packages/back`. We could name it `dist` instead, but I like being consistent with what the CRA build system does.

Let's create a build (and create the `build` folder) with this command:

```sh
npx babel src -d build
```

It works! We can reference this command as a `build` script in `package.json` for convenience (`yarn build`). The build can be run via `node build/index`.

```
  ...
  "scripts": {
    "build": "npx babel src -d build",
    "start": "nodemon --exec ./node_modules/@babel/node/bin/babel-node.js src/index"
    "test": "echo \"Error: no test specified\" && exit 1",
  },
  ...
```

While we're at it, let's add the `build` folder to `.gitignore`.

#### Tests setup

We'll use these:
* [Jest](https://jestjs.io/),
* [supertest](https://github.com/visionmedia/supertest) which will allow testing the Express routes (integration testing)

```sh
yarn add --dev jest supertest
```

Then specify `jest` as the `test` script in `package.json`.

Let's also create a `test` folder where we'll put our tests. We'll see later on how to organize our test files inside that folder.

Let's write a first test, `app.integration.test.js`, inside that folder.

```javascript
// test/app.integration.test.js
import request from 'supertest';
import app from '../src/app';

describe('app', () => {
  it('GETs / and should obtain { foo: "bar" }', async () => {
    expect.assertions(1);
    const res = await request(app)
      .get('/')
      .expect(200);
    expect(res.body).toMatchInlineSnapshot();
  });
});
```

There are two important things to note here.

1. we import `app` from `../src/app`, which doesn't exists. We indeed have to split `src/index.js` into two distinct files.
2. see the `toMatchInlineSnapshot()` call at the end of the test? Jest will automatically fill in the parentheses with the expected return values.

Let's address the first.

The new `app.js` file will export the Express app, so that it can be imported from both the test file _and_ the index file:

```javascript
// src/app.js
import express from 'express';
import bodyParser from 'body-parser';

const app = express();

module.exports = app;
```

The modified `index.js` file will import it and start the server:

```javascript
// src/index.js
import app from './app';

const port = process.env.PORT || 5000;

app.listen(port, (err) => {
  if (err) {
    console.error(`ERROR: ${err.message}`);
  } else {
    console.log(`Listening on port ${port}`);
  }
});

```

We check that `yarn start` and `yarn build` still function, then try `yarn test`.

For some reason, we get a `ReferenceError: regeneratorRuntime is not defined` if we don't configure Babel properly.

We actually have to rename `.babelrc` to `babel.config.js`, and modify its content to (see [Using Babel](https://github.com/facebook/jest#using-babel) in Jest docs):

```javascript
module.exports = {
  presets: [['@babel/preset-env', {targets: {node: 'current'}}]],
};
```

This solves the problem. Now the test runs but, of course, fails: no routes are defined in the Express app, so we need to add a '/' route in `app.js`:

```javascript
// ...
const app = express();

app.get('/', (req, res) => res.json({ foo: 'bar' }));
// ...
```

We still get an error:

    Cannot find module 'prettier' from 'setup_jest_globals.js'

      at Resolver.resolveModule (node_modules/jest-resolve/build/index.js:259:17)

Which brings us back to the **second** point. In order to automatically modify code in the test, Jest uses [Prettier](https://prettier.io/), which ensures consistent formatting. Obviously `prettier` is missing here, so let's install it:

```sh
yarn add --dev prettier
```

Let's run `yarn test` again: it passes. But if we have a look at `test/app.integration.test.js`, we see that Prettier applied formatting that isn't consistent with the Airbnb coding style we chose to follow. Fixing that is as easy as creating a [Prettier config file](https://prettier.io/docs/en/configuration.html), `.prettierrc.js`:

```javascript
// .prettierrc.js
module.exports = {
  trailingComma: 'es5',
  tabWidth: 2,
  semi: true,
  singleQuote: true
};
```

We remove the code that was added by the previous test inside `toMatchInlineSnapshot` call's parentheses, and run the test again. This time the formatting is consistent with our coding style.

We're done with this, let's stage and commit (see [here](https://github.com/bhubr/express-react-monorepo/commit/b79f5fa27e1a3606ad9548b5567349e615e5e6df)).

#### ESLint setup

We'll setup ESLint for Node.js with Airbnb style.

```sh
yarn add --dev eslint
npx eslint --init
```

Let's answer the questions:
* How would you like to use ESLint? **To check syntax, find problems, and enforce code style**
* What type of modules does your project use? **JavaScript modules (import/export)**
* Which framework does your project use? **None of these**
* Does your project use TypeScript? **N**
* Where does your code run? **Node**
* How would you like to define a style for your project? **Use a popular style guide**
* Which style guide do you want to follow? **Airbnb (https://github.com/airbnb/javascript)**
* What format do you want your config file to be in? **JavaScript**
* Would you like to install them now with npm? (Y/n) **N** (again, we'll install them with Yarn)

Then install the deps:

```sh
yarn add --dev eslint-config-airbnb-base@latest eslint@6.2.2 eslint-plugin-import@^2.18.2
```

Then add a `"lint": "npx eslint --fix *.js src test *.js",` under `scripts` in `package.json`.

Running `yarn lint` for the first time, we get a few errors. We need to:
* use the `bodyParser` import in `app.js`,
* add `jest: true` under `env` in `.eslintrc.js`

As a result, we only have the `no-console` left, which will be good enough for now (we could setup a proper logger later). Let's save that ([commit](https://github.com/bhubr/express-react-monorepo/commit/c6b7fef0aa7e21b67d880a4e0b0c3de2b275dc2d)).

#### We're done (for now)

That step was long! Don't worry, we're almost done!

Let's squash-merge the `setup-backend` branch into `master` via a [PR](https://github.com/bhubr/express-react-monorepo/pull/2), then pull `master`.

### Pre-commit hooks setup

#### Husky install

We're gonna setup pre-commit hooks with Husky, so that linting and tests are carried out on every pre-commit event.

```sh
git checkout -b setup-husky
```

Let's get back to the repo root and install Husky:

```sh
yarn add --dev husky
```

Let's commit at this point ([here](https://github.com/bhubr/express-react-monorepo/commit/70133af1fa2187e4630f226d3817e4ba4d30e39d)).

#### `lint-staged` setup

In each of `front` and `back` packages, we're gonna install `lint-staged`, which as the name implies, lints the _staged_ files before committing.

```sh
cd packages/front
yarn add --dev lint-staged
cd ../back
yarn add --dev lint-staged
```

In the `package.json` of each package, we add a `lint-staged` section. `back` and `front` differ slightly, by the paths to check.

What it does is:
* run `yarn lint`, which fixes auto-fixable errors, but prevents for going further if a more serious error occurs.
* stage files again

Here is the `front` version:

```
...
"lint-staged": {
  "src/**/*.js*": [
    "yarn lint",
    "git add"
  ]
}
...
```

Here is the `back` version:

```
...
"lint-staged": {
  "**/*.js": [
    "yarn lint",
    "git add"
  ]
}
...
```

Still in `package.json`, add a `precommit` script (same for `back` and `front`) to run `lint-staged`:

```
  ...
  "scripts": {
    ...
    "precommit": "lint-staged",
    ...
  }
  ...
```

In `front` and `back` packages, we can test this setup by adding errors to `App.jsx` and `app.js`, respectively (like declaring an unused variable).

Then we can `git add` these files to stage them, then run `yarn precommit`, which should trigger an error. After that, we can revert these files to their previous states, and `git add` them again.

At this point, pre-commit scripts are set up, but we need to actually run them on pre-commit events. Let's commit before getting there ([commit](https://github.com/bhubr/express-react-monorepo/commit/3ac3a5ba315f5ea47f86627adb7a2262078306d3)).

#### Husky setup

Back at the repo root, let's add a `husky` section to `package.json`:

```
  ...
  "husky": {
    "hooks": {
      "pre-commit": "npx lerna run --concurrency 1 --stream precommit"
    }
  }
  ...
```

It's worth explaining what this does. On each pre-commit event, the `npx lerna run --concurrency 1 --stream precommit` is run.

`npx lerna run <script>` will run `<script>` in each of the packages. We add these flags:

* `--stream` in order to get console output from the scripts as it's emitted
* `--concurrency 1` to run the scripts from each package sequentially.

Now the pre-commit hooks are configured, and if there are linting errors, we won't be able to commit before fixing them.

Let's `git add` and commit everything ([here](https://github.com/bhubr/express-react-monorepo/commit/771fc9ad037c2d7fb8b8b19399895f628f59dd8c)).

Hold on, we're not done yet, we also want the tests to be run on pre-commit hooks!

#### Trigger tests on pre-commit hooks

We have to update the `precommit` script in each packages's `package.json`, to run both `lint-staged` and `test`:

```
  ...
  "precommit": "lint-staged && yarn test"
  ...
```

Additionnally, we want to prevent tests to running in watch mode in React app (which is the default set by CRA).
This requires amending the `test` script, in frontend app's `package.json`. See [this comment by Dan Abramov](https://github.com/facebook/create-react-app/issues/1137#issuecomment-279180815).

We install `cross-env` to have a working cross-platform setup:

```sh
yarn add --dev cross-env
```

And update `package.json` accordingly, replacing `react-scripts test` with `cross-env CI=true react-scripts test --env=jsdom` for the `test` script.

We make both the back-end and front-end tests fail by making dummy changes to the apps.

For example, in the React app (`App.jsx`), let's amend the `<h1>`'s content:

```jsx
<h1>Hello World { { foo: 'bar' } }</h1>
```

In the Express app (`app.js`), let's change what's returned by the '/' route:

```javascript
app.get('/', (req, res) => res.json({ foo: 'buzz' }));
```

Then we stage everything and try to commit. We end up with an error, which is great!

    lerna ERR! yarn run precommit exited 1 in 'back'
    lerna WARN complete Waiting for 1 child process to exit. CTRL-C to exit immediately.
    husky > pre-commit hook failed (add --no-verify to bypass)

After reverting the apps to their working state, we're all set! Let's commit this ([here](https://github.com/bhubr/express-react-monorepo/commit/9dd7a2ba79bed26c37d6cd2ba575779634ce40cd)).

We can conclude this step by squash-merging the `setup-husky` branch into `master` ([PR](https://github.com/bhubr/express-react-monorepo/pull/3) and resulting [commit on master](https://github.com/bhubr/express-react-monorepo/commit/7dd494875e5caf672dab293111f4b6ec0e31b229)).

### Connect backend and frontend apps

In this final step, we're gonna setup two additional things:
* Fetch data from the backend in the React app
* Setup the backend app in order to expose the React build

First let's create a branch to work on this.

```sh
git checkout -b setup-back-front-connection
```

#### Fetch data from the backend

Let's start with amending the integration test. We'll fetch data from the `/api/foo` endpoint instead of `/`. We then have to update `app.js` accordingly.

Then let's head to the `front` package.

First we'll add `"proxy": "http://localhost:5000"` to `package.json`. Then we'll fetch the `/api/foo` endpoint from the `App` component.

Here's the updated `App.jsx` file:

```jsx
import React, { useState, useEffect } from 'react';

function App() {
  const [foo, setFoo] = useState('N/A');
  useEffect(
    () => {
      fetch('/api/foo')
        .then((res) => res.json())
        .then((data) => setFoo(data.foo))
        .catch((err) => setFoo(err.message));
    },
  );
  return (
    <div>
      <h1>Hello World</h1>
      <p>
Server responded with foo:
        {foo}
      </p>
    </div>
  );
}

export default App;

```

Last, in the root-level `package.json`, we add a `scripts` section:

```
...
  "scripts": {
    "lint": "lerna run lint --stream",
    "start": "lerna run start --stream"
  },
...
```

Now when we run `yarn start`, Lerna will run the `start` script in both `back` and `front` packages, which means we can launch our full-stack app in a single command-line (and a single terminal window!). Same for `yarn lint`!

Let's [commit](https://github.com/bhubr/express-react-monorepo/commit/8beb687cfa6f52a24505497865d9de456d7ea98a) this and move on.

#### Serve the React production build

We're gonna have to amend the `app.js` file in the `back` package, in order to do the following:
* Compute the absolute path of the `build` folder, which is right under the `front` package.
* Check whether we are in a production environment or not. If it's the case:
  * Setup the `build` folder as a static assets directory
  * Create a wildcard route to serve `build/index.html` for all unmatched paths

Here's the updated `app.js`:

```javascript
// src/app.js
import express from 'express';
import bodyParser from 'body-parser';
import path from 'path';

// Check whether we are in production env
const isProd = process.env.NODE_ENV === 'production';

const app = express();
app.use(bodyParser.json());

app.get('/api/foo', (req, res) => res.json({ foo: 'bar' }));

if (isProd) {
  // Compute the build path and index.html path
  const buildPath = path.resolve(__dirname, '../../front/build');
  const indexHtml = path.join(buildPath, 'index.html');

  // Setup build path as a static assets path
  app.use(express.static(buildPath));
  // Serve index.html on unmatched routes
  app.get('*', (req, res) => res.sendFile(indexHtml));
}

module.exports = app;

```

We'll now build the backend app by running `yarn build`, then move to the `front` folder and run the same command.

Then, going back to our `back` folder, let's start the app in production mode:

```sh
NODE_ENV=production node build/index
```

Visiting http://localhost:5000, we should see our React app, up and running.

Let's [commit](https://github.com/bhubr/express-react-monorepo/commit/d7cba4cdb703bfd74f5f45da51a401952c3a43bc) this.

That's it!

A last [PR](https://github.com/bhubr/express-react-monorepo/pull/4) (resulting [commit on master](https://github.com/bhubr/express-react-monorepo/commit/2b224ec6871675936fb02a53969c7e93d3d361ad)), and we're done!
Let's tag that commit:

```sh
git tag initial-setup
git push --tags
```

## Final thoughts

Setting all this up is a bit tedious and took me quite some time, even though I'd already done something similar before!

So if you don't want to spend precious time, feel free to re-use this setup. I suggest you download an [archive](https://github.com/bhubr/express-react-monorepo/archive/initial-setup.tar.gz) of the `initial-setup` release, instead of forking this repo. This can be used as a starting point for your new project.

I didn't cover every aspect of a project setup, since my focus was more on the ESLint/Jest part. Among the things that we could do to go further:
* Set up Prettier
* Set up a database, with or without an ORM
* Set up `dotenv`

Let me know if that might be of some interest to you guys!

Also, I'd like to hear your thoughts and suggestions on this setup: I'm eager to know about anything you're doing differently, and why!

Thanks for reading!