Monorepo setup using Yarn workspaces

Monorepo setup using Yarn workspaces

Before jumping into what is monorepos and yarn workspaces let's try to understand the problem that we face.

Consider we have an application that has Frontend, backend, and a chrome extension.

The usual structure for it would be something like this

normal usage.png All the 3 separate repos have the same contracts, helper functions and have a common piece of code.

If we need to change a contract, then we'll have to make that change in all the repos.

The same holds good for any logic modification in any of the helper functions.

The problem that we are aiming to solve here is this mandatory change in all the repos for any kind of code modification.

Monorepo is a single repository that stores all the code for your application. Frontend, backend, extensions, common code, and contracts, etc.

Yarn workspaces let you organize your codebase and allows you to abstract out the major logic that might be used in various places or maintain correct contract throughout the app.

If this same application was built in a workspace, then it would be something like this:

monorepo.png

TL'DR

We are gonna create "local" packages for any code that we want to abstract out and share and then use it like regular packages.

Let's get started with a simple setup:

Make a new folder

mkdir monorepo
cd monorepo

# create another folder inside the monorepo
mkdir packages

We'll be having all our code inside the packages folder and in a package.json file we tell yarn about our workspace

touch package.json

package.json

{
    "private": "true",
    "workspaces": ["packages/*"]
}

By specifying packages/* we tell where are our packages located.

Now, let's create a React app inside our packages

yarn create react-app web

And for our backend create another folder called server

mkdir server
cd server

yarn init # to create the package.json file

touch index.ts # create an index.ts file

Now let's create another folder utils. This will serve as a common code/contracts that will be shared by our frontend and our backend application.

mkdir utils
cd utils

yarn init

mkdir src
cd src

touch index.ts

utils/src/index.ts

export const add = (a: number, b:number ) => {
    return a + b;
}

The folder structure should be looking something like

monorepo
-- package.json
-- packages
   -- utils
      -- src
          -- index.ts
      -- package.json
   -- server
      -- index.ts
      -- package.json
   -- web
      -- public
      -- src
      -- package.json

Go to the web directory and add utils as a dependency. The name should be the same as it is in the package.json of utils.

Now, install the dependencies again for web.

Go to your App.js and import add() that we defined in the utils

// ...
import { add } from "utils";

function App() {

  return (
    <div className="App">
      <header className="App-header">
        <p>
          {add(2,3)}
        </p>
      </header>
    </div>
  );
}

export default App;

Now, run the start the dev environment for the frontend with

yarn workspace web start

The syntax is yarn workspace [project-name] start

The project name is to be picked up from the package.json of the project.

Go to localhost:3000, you'll be able to see the result 5

Awesome !!! This part is working all good.

Let's head to the backend now.

Go to the server directory and add utils as a dependency in package.json, similar to how we did in web.

Now go to the index.ts file and add this code.

import { add } from "utils";

console.log("This is the server file, ", add(1,2))

Lets run it

node index.ts

You'll be able to see your console from the commonFunction as well as the console from index.

And just like that you have shared a piece of code among two sub-projects.

Improvements

  • You can name all the sub-projects in a better way in their respective package.json

    utils@monorepo/utils

    web@monorepo/web

    server@monorepo/server

  • For starting the dev environments, in the package.json in monorepo directory add these scripts

{
   "private": true,
   "name": "monorepo",
   "workspaces": ["packages/*"],
   "scripts": {
       "client": "yarn workspace @monorepo/web start",
       "server": "yarn workspace @monorepo/server start",
   }
}

Another advantage of this codebase structure is that there is only 1 node_modules created at the top level.

If you open the node_modules then you'll see a package called @monorepo which would contain all the packages that are inside your packages folder.

Another tool that would be helpful here is Lerna.

In monorepo dir's package.json, add Lerna as a dependency and add another script

"build": "lerna run build"

Now, add a lerna.json file

{
    "lerna": "2.11.0",
    "packages": ["packages/*"],
    "version": "0.0.0",
    "npmClient": "yarn",
    "useWorkspaces": true
  }

To run with lerna, before using the packages run the script yarn build and lerna will register the available packages.

To set up the initial structure you can also use lerna init

And just like that, now you have a better codebase for your project !!!

You can play around with the code in this github repo