Monorepo with NPM workspaces

I recently converted a project into a Monorepo.
I had a cli part and an Astro StaticSiteGenerator part.
At some point I felt like these parts would be entangled too much so I decided to separate them. Since they were still related they should stay in one repo but have their own dependencies and separate processes. I still could have kept this in one Astro project with a cli folder but so it feels cleaner structured.

I chose NPM as my package manager since it comes bundled with node and so I decided to try out NPM workspaces as my MonoRepo approach.

So here are my takeaways

1. Setup

  • The NPM workspaces Monorepo has one root dir with one package.json which defines the workspaces, but besides this it is more or less empty. The package.json defines a workspace directory which contains the sub projects.
    As of now you can’t use npm init for this but have to create the main package.json manually.
  "name": "workspaces-example",
  "workspaces": [
    "packages/*"
  ],Code language: JavaScript (javascript)
  • The sub projects each have their own package.json with all necessary dependencies for this specific project. The sub projects can be initialised by npm init -w packages/cli
  • To add dependencies you can add the workspace to the npm add command and the dependency will be added to the respective package.json:
    npm add commander -w packages/cli
    To install dependencies, run npm i from the root directory. This will install all deps of all workspaces in a node_modules folder in the root directory, but none in the projects in the workspaces folder. This already is a win because it will avoid having dependency duplettes.
    However if you have conflicting versions of the same dependency in your packages, you will have a separate node_modules directory with the only the specific version for this single package.


2. Development

  • Updating dependencies can happen just like adding dependencies:
    npm update commander -w packages/cli
  • When you need different versions of a dependency in your workspaces, NPM will create a new node_modules folder in the one divergating workspace with only the dependency where you have the different version.
  • When you need to share code between workspaces you can import code from one package of the workspaces in another package just as a normal dependency from the NPM registry.
    For example when you want to share types in a typescript project you can add a types workspace and just import in the other workspaces files.
    import {Name} from "types";
  • It is probably good practice to add shortcut scripts to your main package.json to run commands of your workspaces. So you don’t need to change to the workspaces directory, which is of course still possible.
  "scripts": {
    "hello-cli": "npm --workspace=@workspaces-example/cli run hello",
    "hello-web": "npm --workspace=@workspaces-example/web run hello"
  }
Code language: JavaScript (javascript)

3. Deploy

  • Deploying from a Monrepo needs to take the different packages into account, which need to be deployed separately but from the same CI config.
    For Github Actions i used a path directive which changes to the package directory and runs instructions there.
    with:
    path: packages/astro
  • Other prefabricated actions like f.e. actions/deploy-pages have a
    working-directory: ./packages/astro
    directive which have the same effect.

You can check out the example project here: https://github.com/ivoba/npm-workspaces-example

One probably does not necessarily need a Monorepo for this example, but it is good to know that Monorepos are quite simple to achieve just with NPM, in case you need to.