Are you struggling with managing multiple projects, each with its own dependency maze? You've probably heard about monorepos as a solution, but setting one up feels like navigating through a complex labyrinth of configuration files and tooling decisions. Perhaps you've tried different package managers and found yourself switching back to npm out of frustration.
Don't worry – you're not alone. Many developers face these exact challenges when trying to set up a monorepo. The good news is that PNPM offers a straightforward solution that addresses common pain points like dependency management, performance issues, and workspace organization.
Why PNPM for Your Monorepo?
Before diving into the setup, let's understand why PNPM stands out. According to discussions in the Node.js community, developers consistently praise PNPM for its superior performance and efficient dependency management. Unlike traditional package managers, PNPM uses a unique symlink strategy that:
Saves significant disk space by storing dependencies in a content-addressable store
Provides true dependency isolation between packages
Offers faster installation times compared to npm and Yarn
Prevents the dreaded "dependency hell" through strict package isolation
Prerequisites
Before we begin, ensure you have:
Node.js installed (version 16.13 or higher recommended)
Git for version control
Basic familiarity with package managers and JavaScript/TypeScript development
Initial Setup
Let's start by creating our monorepo structure. Open your terminal and follow these steps:
# Create and navigate to your project directory
mkdir pnpm-monorepo
cd pnpm-monorepo
# Initialize git
git init
# Create initial .gitignore
echo "node_modules" > .gitignore
Installing PNPM
First, let's install PNPM globally. The recommended way is using Corepack, which comes with Node.js 16.13+:
corepack enable
corepack prepare pnpm@latest --activate
Verify your installation:
pnpm -v
Setting Up Your Monorepo Structure
Now that we have PNPM installed, let's create the basic structure for our monorepo. One of the most common pain points developers face is managing multiple package.json
files with different peer dependencies. We'll address this by setting up a clear workspace structure.
Initialize your project:
pnpm init
# Set the Node.js version requirement
npm pkg set engines.node=">=16.13.0"
# Specify the package manager
npm pkg set packageManager="pnpm@latest"
# Set type as module for ES modules support
npm pkg set type="module"
Create a workspace configuration file:
# Create pnpm-workspace.yaml
echo "packages:
- 'packages/*'
- 'apps/*'" > pnpm-workspace.yaml
# Create directories for your packages and applications
mkdir packages apps
Setting Up Development Tools
To ensure consistency across your monorepo, let's set up essential development tools. Based on community recommendations, we'll implement:
Prettier for code formatting:
# Install Prettier
pnpm add -D prettier
# Create Prettier configuration
echo '{
"singleQuote": true,
"trailingComma": "es5",
"printWidth": 80,
"tabWidth": 2
}' > .prettierrc.json
# Create Prettier ignore file
echo "coverage
public
dist
pnpm-lock.yaml
pnpm-workspace.yaml" > .prettierignore
ESLint for code quality:
# Install ESLint and its dependencies
pnpm add -D eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin
# Create ESLint configuration
echo '{
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended"
],
"parser": "@typescript-eslint/parser",
"plugins": ["@typescript-eslint"],
"root": true
}' > .eslintrc.json
Creating Your First Packages
Now that we have our basic structure and tools in place, let's create our first packages. A common setup includes shared utilities and frontend applications. We'll address the need for efficient type-sharing between frontend and backend in this example.
Creating a Shared Package
Navigate to the packages directory and create a common utilities package:
cd packages
mkdir common
cd common
pnpm init
Update the
package.json
for the common package:
{
"name": "@your-org/common",
"version": "1.0.0",
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"build": "tsc",
"watch": "tsc -w"
}
}
Add TypeScript configuration:
# Create tsconfig.json
echo '{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"declaration": true,
"outDir": "./dist",
"strict": true,
"moduleResolution": "node",
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}' > tsconfig.json
Creating a Frontend Application
Navigate to the apps directory and create a new React application:
cd ../../apps
mkdir web-app
cd web-app
pnpm init
Install necessary dependencies:
pnpm add react react-dom
pnpm add -D @types/react @types/react-dom typescript vite @vitejs/plugin-react
Update the
package.json
to include the common package:
{
"name": "@your-org/web-app",
"dependencies": {
"@your-org/common": "workspace:*",
"react": "^18.2.0",
"react-dom": "^18.2.0"
}
}
Managing Dependencies Effectively
One of the biggest challenges in monorepos is managing dependencies across multiple packages. Here are some best practices to follow:
Use Workspace Protocol: Always use the
workspace:*
protocol when referencing internal packages:
{
"dependencies": {
"@your-org/common": "workspace:*"
}
}
Manage Peer Dependencies: For shared packages, clearly specify peer dependencies to avoid version conflicts:
{
"peerDependencies": {
"react": "^18.0.0",
"react-dom": "^18.0.0"
}
}
Advanced Configuration and Best Practices
To address the concerns about dependency control and package isolation, let's implement some advanced configurations:
Setting Up Git Hooks and Commit Validation
Install necessary tools:
pnpm add -D husky @commitlint/cli @commitlint/config-conventional lint-staged
Initialize Husky:
pnpm exec husky init
Create commit message validation:
# Create commitlint config
echo "export default { extends: ['@commitlint/config-conventional'] };" > commitlint.config.mjs
# Add commit-msg hook
echo "npx --no -- commitlint --edit \${1}" > .husky/commit-msg
Scripts for Common Tasks
Add these helpful scripts to your root package.json
:
{
"scripts": {
"build": "pnpm -r build",
"dev": "pnpm -r dev",
"lint": "pnpm -r lint",
"test": "pnpm -r test",
"clean": "pnpm -r clean"
}
}
Troubleshooting Common Issues
Here are solutions to common problems you might encounter:
Dependency Resolution Issues If you're experiencing dependency conflicts, try:
pnpm store prune # Clean up the store
pnpm install --force # Force reinstall dependencies
Type Definition Problems For TypeScript projects experiencing type definition issues:
Ensure your
tsconfig.json
includes proper path mappingsUse project references for better type resolution
Consider using tRPC for full-stack type safety
Performance Optimization To improve build and development performance:
Use
pnpm
instead ofnpm install
in CI/CD pipelinesImplement proper caching strategies
Consider using Turborepo for build caching
Conclusion
Setting up a monorepo with PNPM doesn't have to be overwhelming. By following this guide, you've created a solid foundation that addresses common pain points like dependency management, type sharing, and workspace organization. The structure we've set up provides:
Efficient dependency management through PNPM's unique approach
Clear separation of concerns with the workspace structure
Strong typing support across packages
Consistent code quality through shared tooling
Remember to regularly update your dependencies and maintain your workspace configuration as your project grows. The effort you put into proper setup will pay dividends in improved development efficiency and easier maintenance.