Monorepo using pnpm workspaces

Fazal e Rabbi
4 min readJul 7, 2024

--

This article is the second article of my Monorepo in Javascript Series. In this article, I’m going to delves into the use of pnpm, a fast and efficient package manager, for managing monorepos, offering a step-by-step guide to setup and management.
Post Series Roadmap
1 — What is a Monorepo
2 — Monorepo using pnpm workspaces
3 — How to Setup Nx in a pnpm Workspace

Introduction

Managing dependencies and code for multiple projects can be challenging. pnpm, a fast and efficient package manager, offers robust support for monorepos, making it an excellent choice for managing complex codebases. This article explores how to set up and manage a monorepo using pnpm.

Why Choose pnpm for Monorepos?

pnpm stands out for its unique approach to handling node_modules, creating a single version of each package and hard-linking them across projects. This results in significant disk space savings and faster installations.

Setting Up a Monorepo with pnpm

Install pnpm: Start by installing pnpm globally if you haven’t already:

npm install -g pnpm

Initialize a Workspace: Create a new directory for your monorepo and initialize a pnpm workspace:

mkdir pnpm-monorepo 
cd pnpm-monorepo
pnpm init

Configure the Workspace: Create a pnpm-workspace.yaml file in the root of your monorepo to define the packages in the workspace:

packages:   
- 'packages/*'
- 'apps/*'

Create Applications: Create apps for react and nest apps. And packages for re-useable libraries:

mkdir apps
mkdir packages

Initialize the React Application: To create the react app using vite run following command from root of monorepo.

 pnpm create vite apps/ui

Initialize the Nest Application: To create the nestjs app using nestjs/cli run following command from root of monorepo.

pnpx @nestjs/cli new apps/api

Run the react(UI) app:

pnpm --filter ui dev

Once the above command will executed you will see the output like:

UI app starts on port 5173

Run the nestjs(api) app:

 pnpm --filter api start:dev

Once the above command will executed you will see the output like:

Api app start on port 3000

Scripts and Automation: Update the package.json file on root. Add some scripts in package.json to start both apps with single command:

"scripts": {
"start:ui": "pnpm --filter ui dev",
"start:api": "pnpm --filter api start:dev",
"start": "pnpm run start:ui & pnpm run start:api",
"build": "pnpm recursive run build",
"test": "pnpm recursive run test"
}
pnpm start:ui // It will run UI app
pnpm start:api // It will run API app
pnpm start // It will run both apps
pnpm test // it will test on all workspaces

Explanation of pnpm commands:

— filter: It will filter the app you want to run in out case ui and api are two apps.

— ui/api: Name of the app

— dev/start:dev: Script in package.json of app. dev is script command in scripts object in ui app. and start:dev is the script command in api app.

recursive: It will run the specifies command like build and test on all workspaces. If some workspace doesn’t have test script, it will ignore it.

Create a Shared Logger Package: Create a new folder for your shared logger package inside the packages folder:

mkdir -p packages/logger
cd packages/logger
pnpm init

Implement the Logger Package: Add a simple logger implementation in packages/logger/src/index.ts:

export const Logger = (message: string) => {
console.log(`${'Logger - ' + new Date().toISOString() + ': ' + message}`)
}

export default Logger;

Add dependencies: Add the dependencies for logger, in our case it’s only typescript. Run the following command on root of project.

pnpm add --filter logger typescript 

Update package.json: package.json file of logger should look like:

{
"name": "@pnpmworkspace/logger",
"version": "1.0.0",
"description": "",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"keywords": [],
"author": "",
"license": "ISC",
"scripts": {
"build": "tsc"
},
"dependencies": {
"typescript": "^5.5.3"
}
}

tsconfig.json:

{
"compilerOptions": {
"target": "ES6",
"module": "commonjs",
"allowJs": true,
"declaration": true,
"outDir": "./dist",
"strict": true,
"moduleResolution": "node",
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src"]
}

Build Logger package (Run from root):

pnpm --filter logger build

Add Logger as dependency(Run from root):

pnpm add ./packages/logger --workspace-root

Above command will add the package logger into root node_module folder.

— workspace-root: It will add package in root node_module folder.

pnpmworkspace/logger has been added in root node_module

Use Logger Package: You can use @pnpmworkspace/logger into both ui and api apps.

import { Logger } from '@pnpmworkspace/logger';

Run both apps:

pnpm start

This ensures that build and test commands are executed for all projects within the monorepo.

Benefits of Using pnpm in a Monorepo

  1. Efficiency: pnpm’s unique handling of dependencies reduces disk usage and speeds up installations.
  2. Consistency: Centralized management of dependencies ensures consistency across all projects.
  3. Scalability: pnpm can handle large and complex monorepos efficiently.

Conclusion

Using pnpm for managing a monorepo simplifies dependency management and improves efficiency. Its unique approach to handling node_modules makes it an excellent choice for projects of any size, ensuring consistency and scalability.

Github repo

--

--

No responses yet