π Translation: Translated from Korean.
TypeScript Monorepo Practical Guide: 10x Efficiency Boost in Package Management with pnpm workspace
Executive Summary
- Before: 300 lines of duplicate type definitions across 3 projects, 20-second webpack builds
- After: Monorepo integration, zero duplicate lines with type sharing, 2-second tsup builds (10x improvement)
- Key: pnpm workspace for inter-package dependency management, tsup for ultra-fast builds
- Results: 90% build time reduction, 100% type safety, 67% disk usage reduction
π₯ The Pain of Multi-repo: Have You Experienced This?
Scenario 1: Type Definition Copy-Paste Hell
// project-cli/src/types.ts
export interface PostMetadata {
title: string;
slug: string;
// ... 50 lines
}
// project-core/src/types.ts
export interface PostMetadata { // Same code duplicated π
title: string;
slug: string;
// ... 50 lines
}
// project-shared/src/types.ts
export interface PostMetadata { // Duplicated again... ππ
title: string;
slug: string;
// ... 50 lines
}
Problems:
- Same code duplicated across 3 files (300 lines of duplication)
- Adding fields to
PostMetadatarequires updating 3 locations - Inconsistencies emerge during copy-paste (type drift)
Scenario 2: Version Management Hell
# When updating zod version
cd project-cli
npm install zod@latest # 3.22.4
cd ../project-core
npm install zod@latest # 3.22.4
cd ../project-shared
npm install zod@latest # 3.22.4
# Repeated 3 times... π
Problems:
- Dependency updates repeated 3 times
- Risk of installing different versions in each project
- Managing 3 package-lock.json files
Scenario 3: Build Speed Hell
# Building with webpack (20 seconds each)
cd project-cli && npm run build # 20 seconds
cd ../project-core && npm run build # 20 seconds
cd ../project-shared && npm run build # 20 seconds
# Total: 1 minute... π
Problems:
- Total build time of 1 minute
- Slow HMR (Hot Module Replacement) during development
- CI/CD pipeline bottleneck
The Promise of Monorepo
Modify once, synchronize all packages:
- Type definitions: Once in
packages/shared - Dependency updates: Once from root
- Build: Entire build with single
pnpm buildcommand
π― What is a Monorepo?
Definition
Monorepo (Monolithic Repository): A software architecture pattern that manages multiple projects (packages) in a single Git repository
β Multi-repo (Before):
project-cli/ (separate Git repo)
project-core/ (separate Git repo)
project-shared/ (separate Git repo)
β
Monorepo (After):
monorepo/
βββ packages/
β βββ cli/ (package)
β βββ core/ (package)
β βββ shared/ (package)
βββ package.json (root)
Real-World Examples
Global companies using Monorepo:
- Google: 2 billion lines of code in a single repository (using Bazel)
- Facebook: Managing React, React Native, Jest, etc. in Monorepo
- Uber: Integrating hundreds of microservices in Monorepo
- Microsoft: Developing TypeScript, VS Code in Monorepo
Advantages (5 Key Benefits)
1. Code Reusability
// β
Monorepo: Define once in one place
// packages/shared/src/types.ts
export interface PostMetadata { ... }
// packages/cli/src/index.ts
import { PostMetadata } from '@blog/shared';
// packages/core/src/api.ts
import { PostMetadata } from '@blog/shared'; // Using same type
2. Atomic Commits
# β
Update all packages in a single commit
git commit -m "feat: Add author field to PostMetadata"
# β Multi-repo: 3 separate commits
git commit -m "feat: Add author to PostMetadata" # repo1
git commit -m "feat: Add author to PostMetadata" # repo2
git commit -m "feat: Add author to PostMetadata" # repo3
3. Unified CI/CD
# β
Single CI/CD pipeline
- run: pnpm install
- run: pnpm build
- run: pnpm test
# β Multi-repo: Managing 3 pipelines
4. Consistent Tooling
// β
Monorepo: Centralized management from root
{
"devDependencies": {
"typescript": "^5.3.0", // All packages use same version
"prettier": "^3.1.0"
}
}
5. Easy Refactoring
// β
Monorepo: Single "Rename Symbol" in IDE
// packages/shared/src/types.ts
export interface PostMetadata { ... } // Rename
// packages/cli, core all auto-update β
Disadvantages (3 Considerations)
1. Repository Size
- Increased Git clone time (mitigated with shallow clone)
- Complex history management
2. Build Complexity
- Need to consider inter-package dependency order
- Incremental Build configuration required
3. Learning Curve
- Learning Monorepo tools (pnpm, Nx, Turborepo)
- Adapting to new workflows
Decision-Making Guide
When to use Monorepo:
- β 3+ related projects
- β Frequent code sharing between packages
- β Need for unified deployment
- β Need for atomic commits
When to use Multi-repo:
- β Completely independent projects
- β Fully separated teams
- β Completely different deployment cycles
Conclusion: 3+ related projects = Monorepo strongly recommended
π οΈ Technology Stack Choice: pnpm + tsup
pnpm vs npm vs yarn
Disk Usage Comparison:
npm: 900MB (3 projects)
yarn: 850MB
pnpm: 300MB (symlinks) β
67% reduction
How pnpm Works:
~/.pnpm-store/ # Global storage (saved once)
βββ [email protected]/
monorepo/
βββ node_modules/
βββ .pnpm/
βββ [email protected]/ β ~/.pnpm-store/[email protected]/ (symlink)
Advantages:
- Disk Efficiency: No duplicate storage of same packages
- Fast Installation: Reuses cached packages
- Native Workspace Support: Built-in workspace protocol
Benchmark (3 packages, 50 dependencies):
npm install: 30 seconds
yarn install: 25 seconds
pnpm install: 5 seconds β
6x faster
tsup vs webpack vs rollup
Build Time Comparison:
webpack: 20 seconds (complex config)
rollup: 10 seconds (plugins required)
tsup: 2 seconds (minimal config) β
10x faster
tsup Features:
- esbuild-based: Ultra-fast build tool written in Go
- Zero Config: Works out-of-the-box with default settings
- TypeScript Native: Auto-generates .d.ts files
Configuration Comparison:
β webpack (Complex):
// webpack.config.js (50 lines)
module.exports = {
entry: './src/index.ts',
output: { ... },
module: {
rules: [
{ test: /\.ts$/, use: 'ts-loader' },
// ... 20 more lines
]
},
plugins: [ ... ],
// ...
};
β tsup (Concise):
{
"scripts": {
"build": "tsup src/index.ts --format esm --dts"
}
}
Final Choice: pnpm + tsup
Reasons:
- Speed: 6x faster installation, 10x faster builds
- Simplicity: Minimal configuration
- TypeScript-Friendly: Auto-generates type definitions
- Productivity: Best developer experience
π Practical Implementation: Step-by-Step
Let’s build a real project structure step by step.
Step 1: Project Initialization
# 1. Create directory
mkdir blog-monorepo
cd blog-monorepo
# 2. Initialize pnpm
pnpm init
# 3. Create packages directory
mkdir -p packages/{shared,core,cli}
Step 2: pnpm Workspace Configuration
Create pnpm-workspace.yaml:
# pnpm-workspace.yaml
packages:
- 'packages/*'
Root package.json Setup:
{
"name": "blog-monorepo",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "pnpm --filter @blog/cli dev",
"build": "pnpm -r build",
"clean": "pnpm -r clean",
"typecheck": "pnpm -r typecheck"
},
"devDependencies": {
"typescript": "^5.3.0",
"tsup": "^8.0.1"
}
}
Explanation:
private: true: Won’t publish to npmpnpm -r: recursive (all packages)pnpm --filter: Run specific package only
Step 3: Create Package (shared)
cd packages/shared
pnpm init
packages/shared/package.json:
{
"name": "@blog/shared",
"version": "0.1.0",
"description": "Shared type definitions and utilities",
"main": "dist/index.mjs",
"module": "dist/index.mjs",
"types": "dist/index.d.ts",
"scripts": {
"dev": "tsup src/index.ts --watch --format esm --dts",
"build": "tsup src/index.ts --format esm --dts --minify",
"clean": "rm -rf dist",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"zod": "^3.22.4"
},
"devDependencies": {
"tsup": "^8.0.1",
"typescript": "^5.3.0"
}
}
packages/shared/src/index.ts:
// TypeScript shared type definitions
export interface PostMetadata {
title: string;
slug: string;
excerpt: string;
categories: string[];
tags: string[];
language: 'ko' | 'en';
}
export interface WordPressConfig {
url: string;
username: string;
appPassword: string;
}
Step 4: Create Package (core)
cd ../core
pnpm init
packages/core/package.json:
{
"name": "@blog/core",
"version": "0.1.0",
"description": "WordPress API client core logic",
"main": "dist/index.mjs",
"module": "dist/index.mjs",
"types": "dist/index.d.ts",
"scripts": {
"dev": "tsup src/index.ts --watch --format esm --dts",
"build": "tsup src/index.ts --format esm --dts --minify",
"clean": "rm -rf dist",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@blog/shared": "workspace:*",
"axios": "^1.6.2"
},
"devDependencies": {
"tsup": "^8.0.1",
"typescript": "^5.3.0"
}
}
Key Point: "@blog/shared": "workspace:*"
workspace:*: pnpm workspace protocol- Uses local package as dependency
packages/core/src/index.ts:
// Import types from @blog/shared
import { PostMetadata, WordPressConfig } from '@blog/shared';
// WordPress API client
export class WordPressClient {
constructor(private config: WordPressConfig) {}
async createPost(metadata: PostMetadata, content: string) {
// WordPress REST API call
console.log('Creating post:', metadata.title);
}
}
Step 5: Create Package (cli)
cd ../cli
pnpm init
packages/cli/package.json:
{
"name": "@blog/cli",
"version": "0.1.0",
"description": "WordPress automation CLI tool",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"bin": {
"blog": "./dist/index.js"
},
"scripts": {
"dev": "tsup src/index.ts --watch --format esm --dts",
"build": "tsup src/index.ts --format esm --dts --minify",
"clean": "rm -rf dist",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@blog/core": "workspace:*",
"@blog/shared": "workspace:*",
"commander": "^11.1.0"
},
"devDependencies": {
"tsup": "^8.0.1",
"typescript": "^5.3.0"
}
}
packages/cli/src/index.ts:
#!/usr/bin/env node
// Import from both packages
import { WordPressClient } from '@blog/core';
import { PostMetadata } from '@blog/shared';
import { Command } from 'commander';
const program = new Command();
program
.name('blog')
.description('WordPress automation CLI')
.version('0.1.0');
program
.command('publish')
.description('Publish post')
.action(() => {
console.log('Publishing post...');
});
program.parse();
Step 6: TypeScript Configuration
Root tsconfig.json:
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
}
}
Each package’s tsconfig.json:
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src"
},
"include": ["src/**/*"]
}
Step 7: Install Dependencies
# Navigate to root
cd ../..
# Install all dependencies (creates workspace links)
pnpm install
Execution Result:
Packages: +50
Progress: resolved 50, reused 50, downloaded 0, added 50, done
Verification:
ls -la packages/cli/node_modules/@blog/
# shared -> ../../shared (symlink)
# core -> ../../core (symlink)
Final Directory Structure
blog-monorepo/
βββ packages/
β βββ shared/ # Shared types
β β βββ src/
β β β βββ index.ts
β β βββ dist/ # Build output
β β βββ package.json
β β βββ tsconfig.json
β βββ core/ # Core logic
β β βββ src/
β β β βββ index.ts
β β βββ dist/
β β βββ package.json
β β βββ tsconfig.json
β βββ cli/ # CLI tool
β βββ src/
β β βββ index.ts
β βββ dist/
β βββ package.json
β βββ tsconfig.json
βββ node_modules/ # Unified node_modules
βββ package.json # Root package.json
βββ pnpm-workspace.yaml # Workspace config
βββ pnpm-lock.yaml # Single lock file
βββ tsconfig.json # Root TypeScript config
β‘ Workflow: Practical Usage
Development Mode (Watch)
Watch all packages:
# Run all packages in watch mode
pnpm -r --parallel dev
Watch specific package only:
# Develop cli package only
pnpm --filter @blog/cli dev
# Develop core package only
pnpm --filter @blog/core dev
Watch multiple packages simultaneously:
# Only cli and core
pnpm --filter @blog/cli --filter @blog/core dev
Build
Full build (automatic dependency order):
pnpm build
# Execution order:
# 1. shared (no dependencies)
# 2. core (depends on shared)
# 3. cli (depends on shared, core)
Build specific package only:
pnpm --filter @blog/shared build
Check build output:
ls packages/shared/dist/
# index.mjs (ESM bundle)
# index.d.ts (TypeScript type definitions)
Testing
Full test suite:
pnpm test
Test specific package:
pnpm --filter @blog/core test
Adding Dependencies
Add dependency to specific package:
# Add chalk to cli package
pnpm --filter @blog/cli add chalk
# Add axios to core package
pnpm --filter @blog/core add axios
Add dev dependency to root:
# Shared by all packages
pnpm add -D -w prettier
Useful pnpm Commands
# List all packages
pnpm list --depth 0
# Check specific package dependencies
pnpm why axios
# Clean cache
pnpm store prune
# Check workspace structure
pnpm ls -r --depth -1
π Troubleshooting: Real-World Problem Solving
Problem 1: Misunderstanding workspace:* Protocol
Symptom:
pnpm build
# Error: Cannot find module '@blog/shared'
Cause:
- Misunderstanding
workspace:*protocol - Didn’t run
pnpm install - Symlinks not created
Solution:
# Run pnpm install
pnpm install
# Verify symlinks
ls -la packages/cli/node_modules/@blog/
# shared -> ../../shared β
Explanation:
workspace:*: Use local workspace packagepnpm installautomatically creates symlinks- Actual package is in
packages/shared/directory
Problem 2: Build Order Issues
Symptom:
pnpm --filter @blog/cli build
# Error: Cannot find module '@blog/shared/dist/index.mjs'
Cause:
- When building cli, shared’s
dist/folder doesn’t exist yet - shared hasn’t been built yet
Solution 1: Build in dependency order:
# β Wrong way
pnpm --filter @blog/cli build
# β
Correct way (including dependencies)
pnpm --filter @blog/cli... build
# ^^^
# Include dependencies
Solution 2: Full build from root:
# pnpm analyzes dependency graph and builds in order
pnpm -r build
# Execution order:
# 1. shared (no dependencies)
# 2. core (depends on shared)
# 3. cli (depends on core, shared)
Problem 3: Missing Type Definitions (.d.ts)
Symptom:
// packages/cli/src/index.ts
import { PostMetadata } from '@blog/shared';
// ^^^^^^^^^^^^
// Error: Could not find a declaration file for module '@blog/shared'
Cause:
- Missing
--dtsflag in tsup - TypeScript type definition files not generated
Solution:
β Wrong tsup config:
{
"scripts": {
"build": "tsup src/index.ts --format esm"
// β Missing --dts!
}
}
β Correct tsup config:
{
"scripts": {
"build": "tsup src/index.ts --format esm --dts"
// ^^^^^
// Generate type definitions
}
}
Build output:
ls packages/shared/dist/
# index.mjs (JavaScript bundle)
# index.d.ts (TypeScript type definitions) β
Problem 4: Circular Dependencies
Symptom:
pnpm build
# Error: Circular dependency detected
Cause:
cli β core β cli (circular!)
Diagnosis:
// β Wrong structure
// packages/cli/src/index.ts
import { WordPressClient } from '@blog/core';
// packages/core/src/index.ts
import { CLI } from '@blog/cli'; // Circular dependency!
Solution:
β Correct structure (separate types to shared):
cli β core β shared
β
shared
// packages/shared/src/types.ts
export interface PublishOptions {
draft: boolean;
}
// packages/core/src/index.ts
import { PublishOptions } from '@blog/shared'; // β
// packages/cli/src/index.ts
import { WordPressClient } from '@blog/core';
import { PublishOptions } from '@blog/shared'; // β
Principles:
- shared doesn’t depend on anyone
- core only depends on shared
- cli depends on core and shared
- Never allow reverse dependencies
Problem 5: Relative vs Absolute Paths
Symptom:
// packages/cli/src/commands/publish.ts
import { WordPressClient } from '../../../core/src/index';
// ^^^^^^^^^^^^^^^^^^^^^^^^
// Relative path hell...
Solution:
// β
Use workspace packages
import { WordPressClient } from '@blog/core';
// ^^^^^^^^^^^
// Import by package name
Advantages:
- No need to change import paths when file location changes
- IDE autocomplete support
- Refactoring safety
π‘ Practical Tips
Tip 1: Package Naming Strategy
Use scopes:
{
"name": "@blog/cli"
// ^^^^^ ^^^
// scope package name
}
Advantages:
- Prevent npm conflicts (no global namespace pollution)
- Clear ownership indication
- Better readability in imports
Naming conventions:
@organization/package-name
@blog/shared β
@blog/core β
@blog/cli β
blog-shared β (no scope)
shared β (collision risk)
Tip 2: Dependency Version Management
β Wrong approach (different versions in each package):
// packages/cli/package.json
{
"dependencies": {
"zod": "^3.22.0"
}
}
// packages/core/package.json
{
"dependencies": {
"zod": "^3.21.0" // Different version!
}
}
β Correct approach (centralized management from root):
// Root package.json
{
"devDependencies": {
"zod": "^3.22.4", // Manage in one place
"typescript": "^5.3.0"
}
}
// packages/cli/package.json (remove external dependencies)
{
"dependencies": {
"@blog/shared": "workspace:*" // Only workspace packages
}
}
Tip 3: Development Efficiency Scripts
{
"scripts": {
// Watch specific packages
"dev:cli": "pnpm --filter @blog/cli dev",
"dev:core": "pnpm --filter @blog/core dev",
// Watch multiple packages simultaneously (parallel)
"dev:all": "pnpm -r --parallel dev",
// Build with dependencies
"build:cli": "pnpm --filter @blog/cli... build",
// Clean and rebuild
"rebuild": "pnpm clean && pnpm build",
// Type check (quick validation)
"check": "pnpm -r typecheck"
}
}
Tip 4: CI/CD Optimization
GitHub Actions Example:
name: CI
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
# Install pnpm
- uses: pnpm/action-setup@v2
with:
version: 8
# Install Node.js
- uses: actions/setup-node@v3
with:
node-version: 20
cache: 'pnpm'
# Install dependencies (frozen-lockfile)
- run: pnpm install --frozen-lockfile
# Type check
- run: pnpm typecheck
# Build
- run: pnpm build
# Test
- run: pnpm test
Key Points:
--frozen-lockfile: Prevent lock file changes (reproducibility)cache: 'pnpm': Utilize pnpm cache (faster installation)
Tip 5: Incremental Migration
Migrating from Multi-repo β Monorepo Steps:
Step 1: Create new Monorepo repository
mkdir monorepo
cd monorepo
pnpm init
Step 2: Migrate shared package first
mkdir -p packages/shared
# Move common code to shared
Step 3: Migrate existing repos to packages
git clone ../project-cli packages/cli
git clone ../project-core packages/core
Step 4: Change dependencies to workspace:*
// packages/cli/package.json
{
"dependencies": {
"@blog/core": "workspace:*", // npm version β workspace
"@blog/shared": "workspace:*"
}
}
Step 5: Validate and transition
pnpm install
pnpm build
pnpm test
# Archive old repos if successful
π Performance Metrics: Before/After
Quantitative Comparison
| Metric | Multi-repo (Before) | Monorepo (After) | Improvement |
|---|---|---|---|
| Build Time | webpack 20s | tsup 2s | 10x β |
| Duplicate Code | 300 lines (type definitions copied) | 0 lines (shared package) | 100% eliminated |
| Type Safety | 70% (drift from copying) | 100% (single source) | 30% β |
| Dependency Updates | 3 times (each repo) | 1 time (root) | 3x β |
| Disk Usage | 900MB (duplicate node_modules) | 300MB (pnpm symlinks) | 67% reduction |
| Developer Experience | Inconvenient (3 terminal windows) | Convenient (1 window) | β |
Time Savings by Scenario
Scenario 1: Modifying Type Definitions
Before (Multi-repo):
# Modify 3 files (1 minute each)
vi project-cli/src/types.ts # 1 minute
vi project-core/src/types.ts # 1 minute
vi project-shared/src/types.ts # 1 minute
# Build each (20 seconds each)
cd project-cli && npm run build # 20 seconds
cd ../project-core && npm run build # 20 seconds
cd ../project-shared && npm run build # 20 seconds
# Total: 4 minutes
After (Monorepo):
# Modify once in shared
vi packages/shared/src/types.ts # 1 minute
# Full build (automatic dependency order)
pnpm build # 6 seconds (shared 2s + core 2s + cli 2s)
# Total: 1 minute 6 seconds β
(4x faster)
Time saved: 4 minutes β 1 minute 6 seconds
Scenario 2: Dependency Updates
Before (Multi-repo):
# Update in each project
cd project-cli && npm install zod@latest # 30 seconds
cd ../project-core && npm install zod@latest # 30 seconds
cd ../project-shared && npm install zod@latest # 30 seconds
# Total: 1 minute 30 seconds
After (Monorepo):
# Once from root
pnpm add -D -w zod@latest # 5 seconds
# Total: 5 seconds β
(18x faster)
Time saved: 1 minute 30 seconds β 5 seconds
π― Conclusion
Key Takeaways
Core points of this TypeScript Monorepo practical guide:
- β Problem Recognition: Multi-repo pain points (duplicate code, version management hell, slow builds)
- β Technology Choice: pnpm workspace (67% disk reduction) + tsup (10x builds)
- β Practical Implementation: Step-by-step guide (workspace setup β package creation β dependency management)
- β Workflow: Master development/build/test commands
- β Troubleshooting: 4 real-world problem solutions
- β Performance Metrics: Builds 20s β 2s, duplicate code 300 lines β 0 lines
What You’ll Gain
Immediately:
- 90% build time reduction (20s β 2s)
- 3x dependency management efficiency (3 times β 1 time)
Short-term (1 week):
- 100% type safety achieved
- Complete elimination of duplicate code
Long-term (1 month):
- 30% developer productivity boost
- Improved code quality (single source of truth)
Next Steps
Week 1: Experimentation
- Build Monorepo with test project
- Master pnpm workspace
- Master tsup build configuration
Week 2: Migration Preparation
- Analyze existing project dependencies
- Identify shared package candidates
- Develop migration plan
Week 3: Transition
- Execute incremental migration
- Update CI/CD pipeline
- Train team members
Week 4: Optimization
- Configure build cache
- Optimize incremental builds
- Measure performance and document
Additional Learning Resources
Official Documentation:
Advanced Topics:
- Turborepo: Caching and remote builds
- Nx: Large-scale Monorepo management
- Changesets: Version management and automated CHANGELOG
Start your TypeScript Monorepo journey today! π
10x build improvements, zero duplicate code, 100% type safety – it’s all possible.
Have questions or feedback? Leave a comment!
I’d love to hear about your Monorepo implementation experience too. π
Leave A Comment