IzioDev

Go to English language
Published on

You saw mistakes in this article? please report at contact@izio.fr

How to properly manage a Node.js monorepo

Managing a Node.js mono repository is not simple. There is a lot of tools to solve this problem. But what is the proper way to actually succeed in creating a mono repository project?

Image illustrating How to properly manage a Node.js monorepo

What a monorepo is?

If you still didn't hear about mono repository despite all the articles on the subject, then you live in a cave. A mono repository is a repository that contains every project or sub-project of a structure.

The other way to maintain projects under a version control system (VCS) is to split projects into different repositories.

Do you see this forest picture? it's my mono repository and every tree is a subfolder containing a project. In comparison, having multiple forests that contain only one tree, is a multi-repository architecture. I am not going to compare the two ways of managing projects for two reasons:

  • It depends on what you want to achieve
  • Internet has plenty of articles speaking of this subject (⚠️ a lot of these articles are biased)

The Example

Considering this project tree:

nodejs-monorepo-example
 ┣ packages
 ┃ ┣ api
 ┃ ┃ ┣ index.js
 ┃ ┃ ┗ package.json
 ┃ ┣ maths
 ┃ ┃ ┣ index.js
 ┃ ┃ ┗ package.json
 ┃ ┗ website
 ┃ ┃ ┣ index.js
 ┃ ┃ ┗ package.json
 ┗ package.json

Note: To make it simple, we'll be using pure Javascript here. But the same is doable in Typescript, in fact, this website has been built using this architecture in Typescript.

We have one packages folder containing every sub-project. maths package will contain cross-package sharable code.

In order to generate package.json file, you can use npm init. Replace npm with your favourite package manager (yarn, pnpm, etc.).

The differents ways

There are multiple ways to achieve a monorepo architecture in Node.js, from what I know there are three simple methods :

I tried using the first two solutions, but they most likely will not fit all needs. You are free to use one of these three methods. However, I'll show you how to set up the workspace only on pnpm.

The proper way

pnpm is a package manager made in Typescript that focuses on performance, efficiency and (guess what?) monorepo! 🎉

pnpm offers many installation methods. I assume you already have a package manager: npm install -g pnpm.

Let's create a workspace definition file pnpm-workspace.yaml:

1# pnpm-workspace.yaml 2 3packages: 4 - "packages/**"

In our example, this file isn't needed because by default pnpm assume all packages of all subdirectories are included.

The next step is to name our packages. Edit all package.json files and make an alias name so you remember it's an internal package:

1{ 2 "name": "@example/maths", 3 "version": "1.0.0", 4 "description": "An internal maths package", 5 "main": "index.js", 6 "scripts": { 7 "start": "node index.js" 8 }, 9 "author": "IzioDev", 10 "license": "ISC" 11}

Now, we can share the code located in one package with another package. Try to run pnpm add @example/maths while you're in /packages/website.

🤔 It's weird, it's not saying that the package wasn't found on the npm registry. Yes, correct, by default pnpm look all over your packages before looking for an npm package. You can check this assertion by running pnpm add @example/test and how it's looking for an npm package.

Let's check the website package.json file:

1// packages/website/package.json 2 3"dependencies": { 4 "@example/maths": "workspace:^1.0.0" 5}

It created a new dependency with a pnpm syntax. But don't worry, if you need these packages to be published on the npm registry, at publish time, this dependency would become :

"@example/maths": "^1.0.0"

Note: Since now, we've only used Common JS, However, if you want to use ESM, just add "type": "module" to all package.json so that node can know it's an ES module.

We can now create an exported add function into /maths/index.js:

1// packages/maths/index.js 2 3// Common JS 4const add = (a, b) => a + b; 5 6module.exports = { 7 add, 8}; 9 10// Or, ESM 11export const add = (a, b) => a + b;

Edit /website/index.js to import our freshly created add function:

1// packages/website/index.js 2 3// Common JS syntaxe 4const add = require("@example/maths").add; 5 6// Or, ESM syntaxe 7import { add } from "../maths"; 8 9// Use the imported function 10const result = add(1, 2); 11 12console.log(`The result is: ${result}`);

Since we're lazy developers, we don't want to change directory to /packages/website every time in order to run the entry file, right? So let's tweak our package.json at the project root directory:

1// package.json 2 3"scripts": { 4 "start:website": "pnpm -C packages/website start" 5}

The -C option is to tell pnpm we want to run the command on another path, so here: on packages/website, and we're running the start command, as you would probably do using npm.

Ok, let's try this all together pnpm start:website at the project root directory will output:

The result is: 3

No really? 🤔 Thanks node for being so smart.

Conclusion

Well, you have everything you need to set up a Node.Js monorepo and manage your internal packages properly.

In this article, we've:

  • Installed pnpm
  • Seen the different ways (npm, yarn, pnpm workspaces)
  • Created a pnpm workspace with multiple packages
  • Made a package dependent on another
  • Created an utility script at the project root to make our life easier

What's next?

In order to go further, you can try publishing your packages on a registry using the pnpm publish command.

Share