A Fullstack Javascript Monorepo example
Tags: monorepo, javascript, webdev, productivity
Published: 2020-08-26 01:38:30

You are working on a large javascript project. It is full of features and growing by the day.
You have web and mobile clients on the frontend and services of any kind on the backend side.
Every part of your application is somehow coupled with other parts to work and even starting the project in the development phase is a pain ...

If that's the case, hear what I have to say about monorepos.

([source code](https://github.com/alecap7/lerna-monorepo-example))

## What is a Monorepo?
As [Wikipedia](https://en.wikipedia.org/wiki/Monorepo) says:
> A **monorepo** is a software development strategy
> where code for many projects is stored in the same
> repository.

Simple and straightforward.

Here is a typical javascript monorepo structure:
```
repo-root/
  package.json
  projects/
    project-1/
      package.json
    project-2/
      package.json
```
## Tools
When it comes with javascript we have at least two tools to work with to manage our projects in a single repository.

### Yarn

[Yarn](https://yarnpkg.com/) is a well-known dependencies management tool (equivalent to npm). It aims to be also a project management tool by providing multi-package management primitives called [workspaces](https://classic.yarnpkg.com/en/docs/workspaces/):

> Workspaces are a new way to set up your package architecture that’s
> available by default starting from Yarn 1.0. It allows you to setup
> multiple packages in such a way that you only need to run `yarn
> install` once to install all of them in a single pass.

Basically using these features we will have a single *yarn.lock* and a single *node_modules* folder at root level, which means all our project dependencies will be installed together so that yarn will be able to increase performances at installation time. 

Furthermore it allows to define dependencies between internal packages with zero additional configurations.

### Lerna
> A tool for managing JavaScript projects with multiple packages.

[Lerna](https://github.com/lerna/lerna) offers utilities such as the ability to run custom scripts on a specific set of subprojects and integrated solutions for versioning and package publishing based on what has been changed in the repository ([check my post on this topic](https://dev.to/alecap7/lerna-semantic-versioning-simple-project-setup-2k2i)). 

For the sake of completeness, it offers all the features natively implemented by the yarn workspaces but also the possibility of integrating them: we will choose the second option.

For a more exhaustive discussion on yarn, lerna and monorepos I recommend this wonderful [article](https://doppelmutzi.github.io/monorepo-lerna-yarn-workspaces/).

## The sample project
Our sample project is a toy application that fetches some books from the backend and displays them through a web interface.

However, to implement it I have chosen an architecture such that:
- It is a microservices architecture, especially the frontend and the backend will be two separate applications.
- It is also modular, therefore with the possibility of creating packages that can be shared by multiple applications.
- Can be easily enhanced to cover at least one real world use case (this architecture is inspired by the [Storybook Design System Architecture](https://www.learnstorybook.com/design-systems-for-developers/react/en/architecture/))

### Folders structure
We are going to split our projects into two distinct folders: *applications* and *packages*.

The *applications* folder will contain all the components that make up our application at runtime, in our case a graphql *api* and a reactjs *client*.

The *packages* folder will contain modules shared by our applications, in our case a react components package (here called *design-system*).


![enter image description here](https://cdn-images-1.medium.com/max/600/1*9NiEzCZ6HVQYACsajkqDAA.png)
The final folders structure will looks like:
```
repo-root/
  package.json
  packages/
    design-system/
      package.json
  applications/
    client/
      package.json
    api/
      package.json
```
### Yarn/Lerna setup
First you need to setup the management tools for the monorepo. 

Inside the root:
```bash
yarn init
```
***Note***: *yarn workspaces* require the root *package.json* to be private, so during the yarn initialization process make sure to set the private flag to true.

Then we have to install *lerna*:
```bash
yarn add lerna -D
yarn lerna init
```
I always prefer to install this kind of dependencies as devDependencies.

Next we define *yarn workspaces* according to our project structure:
```json
// package.json

{  
  …
  "private": true,
  "workspaces": [
    "applications/*",
    "packages/*"
  ],
  …  
}
```
Then we instruct *lerna* how to integrate itself with *yarn workspaces*:
```json
// lerna.json

{
  ...
  "packages": [
    "applications/*",
    "packages/*"
  ],
  "npmClient": "yarn",
  "useWorkspaces": true,
  ...
}
```
Finally we add a custom script for starting our apps during development:
```json
// package.json

{  
  …
  "scripts": {
    "start": "yarn lerna run development:start --parallel"
  },
  …  
}
```
### Coding the api application
For the backend I chose graphql. In particular we are going to implement the [getting started tutorial of the official apollo website](https://www.apollographql.com/docs/apollo-server/getting-started/) (with the addition of babel to take advantage of the javascript ES6 syntax).

First we have to make a new directory and cd to it:
```bash
mkdir -p applications/api
cd applications/api
```
Then we have to initialize our project dependencies
```bash
yarn init -y
yarn workspace applications/api add @babel/core @babel/cli @babel/node @babel/preset-env nodemon -D
yarn add apollo-server graphql
yarn install
```
and his files and folders
```bash
mkdir src
touch src/index.js
touch .babelrc
```
Next we have to add some configurations. 

Here we define a script to start our graphql app:
```json
// applications/api/package.json

{
  ...
  "scripts": {
    ...
    "development:start": "yarn nodemon --exec babel-node src/index.js ",
    ...
  },
  ...
}

```
Here we define presets for our Babel compiler:
```json
// applications/api/.babelrc

{
  "presets": ["@babel/preset-env"]
}
```
Finally we can add the code:
```js
// applications/api/src/index.js

import { ApolloServer, gql } from "apollo-server";

const typeDefs = gql`
  type Book {
    title: String
    author: String
  }

  type Query {
    books: [Book]
  }
`;

const books = [
  {
    title: "Harry Potter and the Chamber of Secrets",
    author: "J.K. Rowling"
  },
  {
    title: "Jurassic Park",
    author: "Michael Crichton"
  }
];

const resolvers = {
  Query: {
    books: () => books
  }
};

const server = new ApolloServer({ typeDefs, resolvers });

server.listen().then(({ url }) => {
  console.log(`🚀 Server ready at ${url}`);
});
```
You can now test it by running:
```bash
yarn development:start
```
or
```bash
cd ../..
yarn start
```
### Coding the client application
For the client side we are going to build a react web app with an apollo client to work with the graphql backend.

First we bootstrap a new cra project:
```bash
npx create-react-app applications/client
```
Remember we want only one yarn.lock and it has to be placed on the root level, so make sure cra hasn't created a yarn.lock. Otherwise: 
```bash
rm applications/client/yarn.lock
```
Next we install dependencies:
```bash
cd applications/client
yarn add @apollo/client graphql
```
Then we add some configurations:
```json
// applications/client/package.json

{
  ...
  "scripts": {
    "development:start": "CI=true yarn react-scripts start",
    ...
  }
  ...
}
```
Finally, we add the code:
```js
// applications/client/src/App.js

import React from "react";  
import { ApolloClient, ApolloProvider, InMemoryCache } from "@apollo/client";  
import Books from "./components/Books";

const client = new ApolloClient({  
  uri: "http://localhost:4000",  
  cache: new InMemoryCache()  
});

function App() {  
  return (  
    <ApolloProvider client={client}>  
      <Books />  
    </ApolloProvider>  
  );  
}

export default App;
```
Here we are creating the content of our app:
```bash
mkdir src/components
touch src/components/Books.js
```
```js
// applications/client/src/components/Books.js

import React from "react";
import { useQuery, gql } from "@apollo/client";

const ALL_BOOKS = gql`
  query GetAllBooks {
    books {
      title
      author
    }
  }
`;

function Books() {
  const { loading, error, data } = useQuery(ALL_BOOKS);

  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error :(</p>;

  return data.books.map(({ title, author }) => (
    <div key={title}>
      <p>
        {title} by {author}
      </p>
    </div>
  ));
}

export default Books;
```
Test it by running:
```bash
cd ../..
yarn start
```
Note how this will start also the api application.
 
### Coding the design-system package
Here we are going to package some react components.

First we have to make a new directory and cd to it:
```bash
mkdir -p packages/design-system
cd packages/design-system
```
Then we have to init our project and his structure:
```bash
yarn init -y
yarn add react@^16.0.0 -P  
yarn add microbundle-crl -D
```
```bash
mkdir src
touch src/index.js
mkdir src/components
touch src/components/List.js
touch src/components/ListItem.js
```
Next we add some configurations:
```json
// packages/design-system/package.json

{
  ...
  "main": "dist/index.js",
  "module": "dist/index.modern.js",
  "source": "src/index.js",
  "scripts": {
    ...
    "development:start": "yarn microbundle-crl watch --no-compress --format modern,cjs"
    ...
  },
  ...
}
```
Finally, we add the code:
```js
// packages/design-system/src/index.js

import List from "./components/List";

export { List };
```
```js
// packages/design-system/src/components/ListItem.js

import React from "react";
import PropTypes from "prop-types";

// I'm not using css files because they will not work when exported!
// Consider to use styled components for your project...
function ListItem(props) {
  return (
    <div
      style={{
        margin: "10px",
        padding: "10px",
        border: "1px solid #bbb",
        backgroundColor: "#eee"
      }}
    >
      <span
        style={{
          fontSize: "1.2em",
          textDecoration: "none",
          color: "#333"
        }}
      >
        {props.text}
      </span>
    </div>
  );
}

ListItem.propTypes = {
  text: PropTypes.string.isRequired
};

export default ListItem;
```
```js
// packages/design-system/src/components/List.js

import React from "react";  
import PropTypes from "prop-types";  
import ListItem from "./ListItem";

function List(props) {  
  return (  
    <div>  
      {props.items.map((content, index) => (  
        <ListItem key={index} text={content || ""} />  
      ))}  
    </div>  
  );  
}

List.propTypes = {  
  items: PropTypes.arrayOf(PropTypes.string).isRequired  
};

export default List;
```
As last step we have to update the client app:
```js
// applications/client/src/components/Books.js

import React from "react";
import { useQuery, gql } from "@apollo/client";
import { List } from "design-system";

const ALL_BOOKS = gql`
  query GetAllBooks {
    books {
      title
      author
    }
 }
`;

function Books() {
  const { loading, error, data } = useQuery(ALL_BOOKS);
  if (loading) return <p>Loading…</p>;
  if (error) return <p>Error :(</p>;
  return (
    <List
      items={data.books.map(({ title, author }) => `${title} by ${author}`)}
    />
  );
}

export default Books;
```
And its dependencies:
```bash
yarn add design-system@^1.0.0
```
You can now test the final app:
```bash
cd ../..
yarn start
```

***Note***: currently there seems to be a bug with react's development server. After the first start, the page must be refreshed.

## Space for improvements
Our app is so simple that such a complex architecture might seem totally unjustified. 

However, think this way... You want this book listing app to become the best online bookstore in the world!

On the client side, you'll need at least a store app for your customers and a dashboard for you suppliers.

On the server side the underneath data model will explode. You will have to manage your users, track orders and so on. That is, you will have to write tons of business logic line of codes and probably integrations to 3rd party systems. To preserve principles of low-coupling and high-cohesion through your code you'll need to split these logics across many applications and modules.

Your app will probably looks more like this:

![enter image description here](https://cdn-images-1.medium.com/max/800/1*3BuWzOH0LJenf75mdeH4CA.png)

According to the proposed monorepo structure it is easy to scale up the project while keeping your code manageable. You will simply create all the new packages and / or applications you need under the appropriate folders.
   
 
## Conclusions
The disruptive rise of javascript in the field of web development has reached a state of the art in which it is possible to develop very complex applications in a single programming language. 
This situation offers some advantages such as the possibility of centralizing project management partially described here. 

I sincerely hope that my thoughts on this issue will be of help to your current or next project.

Any sort of feedback is very appreciated!