Introduction
Choosing the right monorepo tool is a critical decision that impacts your team's productivity, build performance, and development experience. The three dominant players in the JavaScript/TypeScript monorepo ecosystem—Turborepo, Nx, and Lerna—each take fundamentally different approaches to solving the same problem: managing multiple packages in a single repository efficiently. Understanding their architectural differences, strengths, and trade-offs is essential for making the right choice for your project.
Lerna was the original monorepo tool for JavaScript, pioneering workspace management and package publishing. Turborepo, created by Vercel, focuses on build performance through intelligent caching and parallel execution. Nx, developed by Nrwl, provides a comprehensive development platform with code generation, dependency graph visualization, and distributed task execution. This guide compares these tools across every dimension that matters for production use.
Understanding Monorepo Tools: Core Concepts
What Problem Do They Solve?
Monorepo tools address several challenges that arise when managing multiple packages in a single repository. Without tooling, developers face slow builds that recompile everything, inconsistent dependency versions across packages, manual coordination of build order, and no caching of previous results. These problems compound as the monorepo grows—what works for 5 packages becomes unmanageable at 50.
The core problems include:
| Challenge | Without Tooling | With Tooling |
|---|---|---|
| Dependency Management | Manual workspace configuration | Automatic workspace linking |
| Build Orchestration | Run all builds sequentially | Dependency-aware parallel builds |
| Caching | Rebuild everything | Cache and skip unchanged tasks |
| Package Publishing | Manual versioning and publishing | Automated versioning and publishing |
| Code Sharing | Complex import paths | Simple workspace references |
| Consistency | Inconsistent configurations | Shared tooling and configs |
| Affected Detection | Run all tests always | Only test what changed |
Architectural Approaches
Each tool takes a fundamentally different architectural approach to solving these problems:
Lerna was the pioneer, created in 2015 by Sebastian McKenzie (who later created Nx). Lerna focuses on two things: running commands across packages and publishing packages to npm. It delegates build orchestration and caching to the underlying package manager's workspace feature. Lerna was the de facto standard until Turborepo and Nx emerged with more sophisticated approaches.
// lerna.json
{
"version": "independent",
"npmClient": "yarn",
"useWorkspaces": true,
"command": {
"publish": {
"conventionalCommits": true
},
"version": {
"message": "chore(release): publish"
}
}
}Turborepo takes a build-system-first approach. Created by Jared Palmer (acquired by Vercel in 2021), it focuses entirely on making builds fast through content-addressable caching, parallel execution, and remote cache sharing. Turborepo is deliberately minimal—it configures through a single turbo.json file and uses the package manager's workspace feature for dependency management.
// turbo.json
{
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**"]
},
"test": {
"dependsOn": ["build"],
"outputs": ["coverage/**"]
}
}
}Nx is a full development platform. Created by Nrwl (founded by former Angular team members), Nx provides code generation, dependency graph visualization, affected commands, distributed task execution, and a plugin ecosystem. It's the most feature-rich but also the most complex to adopt.
// nx.json
{
"tasksRunnerOptions": {
"default": {
"runner": "nx-cloud",
"options": {
"cacheableOperations": ["build", "test", "lint"]
}
}
}
}Deep Dive: Each Tool
Lerna — The Original
Lerna focuses on package management and publishing. It was the standard for years but lost momentum when the original maintainer stepped back. In 2022, the Nx team took over Lerna maintenance, bringing modern features like caching.
# Initialize Lerna
npx lerna init
# Create package structure
mkdir -p packages/utils packages/ui apps/web
cd packages/utils && npm init -y
cd packages/ui && npm init -y
cd apps/web && npm init -y
# Root package.json
{
"workspaces": ["packages/*", "apps/*"],
"devDependencies": {
"lerna": "^8.0.0"
}
}
# Run commands across all packages
npx lerna run build
npx lerna run test --scope=@myorg/ui
# Publish with conventional commits (auto-generates changelogs)
npx lerna publish --conventional-commitsLerna's strength is its simplicity and its publishing workflow. With conventionalCommits: true, Lerna analyzes commit messages to automatically determine version bumps (patch for fixes, minor for features, major for breaking changes) and generate changelogs.
// lerna.json with modern Nx-powered caching
{
"version": "independent",
"npmClient": "pnpm",
"useWorkspaces": true,
"command": {
"publish": {
"conventionalCommits": true,
"message": "chore(release): publish"
}
},
"packages": ["packages/*", "apps/*"]
}Turborepo — The Performance Champion
Turborepo's core innovation is content-addressable caching. Instead of tracking which files changed, Turborepo hashes the inputs (source files, dependencies, environment variables, config files) to each task. If the hash matches a previous run, the cached output is restored instantly—even on a fresh CI machine.
// turbo.json — comprehensive configuration
{
"$schema": "https://turbo.build/schema.json",
"globalDependencies": [".env", ".env.local"],
"globalEnv": ["CI"],
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", ".next/**"],
"env": ["NODE_ENV", "API_URL"]
},
"test": {
"dependsOn": ["build"],
"outputs": ["coverage/**"],
"inputs": ["src/**/*.tsx", "src/**/*.ts", "test/**/*.ts"]
},
"lint": {
"outputs": []
},
"dev": {
"cache": false,
"persistent": true
},
"deploy": {
"dependsOn": ["build", "test"],
"cache": false
}
}
}Remote caching is Turborepo's killer feature. When enabled, CI uploads cache artifacts to Vercel's Remote Cache. Every developer and CI machine shares the same cache—so if CI already built a package, your local build skips it entirely:
# Enable remote caching
npx turbo login
npx turbo link
# Now builds use shared cache
pnpm turbo build # "cache hit, replaying output" for unchanged packages
# Only build/test what changed vs main branch
pnpm turbo build --filter='...[origin/main]'
pnpm turbo test --filter='...[origin/main]'# GitHub Actions with Turborepo
name: CI
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v2
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
- run: pnpm turbo build --filter='...[origin/main]'
- run: pnpm turbo test --filter='...[origin/main]'
- run: pnpm turbo lint --filter='...[origin/main]'Nx — The Development Platform
Nx goes far beyond build orchestration. It provides a complete development platform with code generators, dependency graph visualization, affected commands, and distributed task execution:
# Create new Nx workspace
npx create-nx-workspace@latest my-monorepo --preset=ts
# Add applications and libraries using generators
nx g @nx/react:app web
nx g @nx/node:app api
nx g @nx/react:lib ui
nx g @nx/js:lib utils
# Run affected tasks (only what changed)
nx affected --target=build --base=origin/main --head=HEAD
# Visualize the dependency graph
nx graphNx's dependency graph is particularly powerful. It understands not just package.json dependencies but also TypeScript import paths, allowing it to determine exactly which packages are affected by a change:
// nx.json — full configuration
{
"tasksRunnerOptions": {
"default": {
"runner": "nx-cloud",
"options": {
"cacheableOperations": ["build", "test", "lint", "typecheck"],
"accessToken": "your-nx-cloud-token",
"parallel": 3
}
}
},
"targetDefaults": {
"build": {
"dependsOn": ["^build"],
"inputs": ["production", "^production"],
"outputs": ["{projectRoot}/dist"]
},
"test": {
"inputs": ["default", "^production"]
}
},
"namedInputs": {
"default": ["{projectRoot}/**/*", "sharedGlobals"],
"production": ["default", "!{projectRoot}/**/?(*.)+(spec|test).[jt]s?(x)"],
"sharedGlobals": []
}
}Nx's code generators enforce conventions across the monorepo:
# Generate a React component with tests, stories, and exports
nx g @nx/react:component UserProfile --project=web --export
# Generate a NestJS service with controller and module
nx g @nx/nest:service user --project=api
# Generate a full-stack feature (frontend + backend)
nx g @nx/workspace:plugin my-pluginDistributed task execution splits work across multiple CI machines:
# nx.json — distributed CI
# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
main:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: nrwl/nx-set-shas@v4
- run: npm ci
- run: npx nx-cloud start-ci-run --distribute-on="5 linux-medium-js"
- run: npx nx affected -t build test lint --base=origin/mainPerformance Comparison
Performance is often the deciding factor. Here's how the three tools compare on a typical monorepo with 50 packages:
| Metric | Lerna | Turborepo | Nx |
|---|---|---|---|
| Cold build (50 packages) | 45 min | 25 min | 20 min |
| Cached build (50 packages) | 45 min | 2 min | 2 min |
| Affected build (3 packages changed) | 15 min | 5 min | 4 min |
| Remote cache hit | N/A | 30 sec | 30 sec |
| Parallel execution | Basic | Advanced | Advanced |
| Distributed execution | No | No | Yes (Nx Cloud) |
Lerna without caching runs all builds sequentially. Turborepo and Nx both achieve dramatic speedups through caching and parallel execution. Nx goes further with distributed execution across multiple CI machines.
# Turborepo: Concurrency control
pnpm turbo build --concurrency=10
# Turborepo: Prune for Docker (creates minimal subset)
pnpm turbo prune --scope=web --docker
# Nx: Distributed execution
npx nx run-many --target=build --all --parallel=3
# Nx: Analyze dependency graph
npx nx graph --print
# Lerna: Parallel execution
npx lerna run build --parallel
# Lerna: Only changed packages
npx lerna run test --since=origin/mainReal-World Use Cases
Use Case 1: Publishing Library Monorepo with Lerna
For teams maintaining a suite of npm packages (design system, utilities, SDK), Lerna's publishing workflow is unmatched:
// lerna.json
{
"version": "independent",
"npmClient": "pnpm",
"useWorkspaces": true,
"command": {
"publish": {
"conventionalCommits": true,
"message": "chore(release): publish",
"registry": "https://registry.npmjs.org"
}
}
}
// GitHub Actions: automated publishing
name: Release
on:
push:
branches: [main]
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- run: npm ci
- run: npx lerna publish --conventional-commits --yes
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}Use Case 2: SaaS Application with Turborepo
For teams building web applications with Next.js, Turborepo's integration with Vercel and its caching make it ideal:
// turbo.json
{
"$schema": "https://turbo.build/schema.json",
"remoteCache": {
"team": "my-saas-team"
},
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": [".next/**", "!.next/cache/**"]
},
"test": {
"dependsOn": ["build"],
"outputs": ["coverage/**"]
},
"deploy": {
"dependsOn": ["build", "test"],
"cache": false
}
}
}
// GitHub Actions: deploy only affected apps
- name: Deploy affected apps
run: pnpm turbo deploy --filter='...[origin/main]'Use Case 3: Enterprise Platform with Nx
For large organizations with 50+ packages, multiple frameworks, and compliance requirements, Nx provides the governance and tooling needed:
// nx.json — enterprise configuration
{
"tasksRunnerOptions": {
"default": {
"runner": "nx-cloud",
"options": {
"cacheableOperations": ["build", "test", "lint", "typecheck"],
"accessToken": "your-token",
"parallel": 3
}
}
}
}
// Enforce module boundaries with lint rules
// .eslintrc.json
{
"rules": {
"@nx/enforce-module-boundaries": ["error", {
"depConstraints": [
{ "sourceTag": "scope:shared", "onlyDependOnLibsWithTags": ["scope:shared"] },
{ "sourceTag": "scope:web", "onlyDependOnLibsWithTags": ["scope:shared", "scope:web"] },
{ "sourceTag": "scope:api", "onlyDependOnLibsWithTags": ["scope:shared", "scope:api"] }
]
}]
}
}Nx's module boundary enforcement prevents teams from creating circular dependencies or importing code they shouldn't—a critical feature for enterprise governance.
Migration Strategies
From Single Repo to Monorepo
# 1. Identify shared modules
grep -r "import.*from.*shared" src/ | head -20
# 2. Create monorepo structure
npx create-turbo@latest monorepo --pnpm
# 3. Move code into packages
mkdir -p packages/utils
mv src/utils/* packages/utils/src/
# Update package.json with workspace references
# 4. Verify build
pnpm turbo build
pnpm turbo testFrom Lerna to Turborepo
# 1. Install Turborepo (can coexist with Lerna)
pnpm add turbo -Dw
# 2. Create turbo.json
cat > turbo.json << 'EOF'
{
"pipeline": {
"build": { "dependsOn": ["^build"], "outputs": ["dist/**"] },
"test": { "dependsFrom": ["build"] }
}
}
EOF
# 3. Replace lerna run with turbo
# Before: npx lerna run build
# After: pnpm turbo build
# 4. Keep Lerna for publishing
# npx lerna publish --conventional-commitsFrom Turborepo to Nx
# Nx provides a migration command
npx nx init
# This converts turbo.json to nx.json
# Preserves existing workspace structure
# Adds Nx Cloud for distributed cachingBest Practices
-
Start simple, scale gradually: Begin with Turborepo for small monorepos. Migrate to Nx when you need code generation or distributed execution.
-
Enable remote caching immediately: This is the single highest-impact optimization. Both Turborepo and Nx Cloud offer free tiers for small teams.
-
Use affected detection religiously: Never run full builds on PRs. Use
--filter='...[origin/main]'(Turborepo) ornx affected(Nx) to only build what changed. -
Standardize tooling across packages: Use shared ESLint, Prettier, and TypeScript configurations. Both Turborepo and Nx provide starter templates with shared configs.
-
Automate publishing with conventional commits: Use Lerna or Changesets for automated versioning. Write meaningful commit messages so version bumps are automatic.
-
Monitor cache hit rates: Track how often your cache hits. Below 80% means your cache keys are too specific. Above 95% means you might be caching too aggressively.
-
Use the workspace protocol: Always use
workspace:*for internal dependencies. This ensures packages link to source code during development and to published versions in production. -
Document package boundaries: Use CODEOWNERS for review requirements. With Nx, use lint rules to enforce module boundaries.
Common Pitfalls
| Pitfall | Impact | Solution |
|---|---|---|
| No remote caching | Slow CI, redundant builds | Enable Turborepo or Nx Cloud immediately |
| Running full builds on PRs | Wasted CI resources, slow feedback | Use affected detection |
| Inconsistent configurations | Build failures, style drift | Use shared configs (eslint-config, tsconfig) |
| Circular dependencies | Build failures, slow analysis | Use Nx module boundary rules |
| Missing CI for shared packages | Breaking changes go undetected | Always test affected dependents |
| Over-engineering with Nx | Unnecessary complexity for small projects | Start with Turborepo, add Nx when needed |
| Lerna without caching | No performance benefit over manual scripts | Add Nx-powered caching (Lerna 7+) |
Comparison Matrix
| Feature | Lerna | Turborepo | Nx |
|---|---|---|---|
| Primary Focus | Package publishing | Build performance | Development platform |
| Remote Caching | Via Nx integration | Vercel Remote Cache | Nx Cloud |
| Affected Detection | Basic (--since) | Advanced (--filter) | Advanced (nx affected) |
| Code Generation | No | No | Yes (generators) |
| Dependency Graph | Basic | Advanced | Advanced + visual |
| Distributed Execution | No | No | Yes (Nx Cloud) |
| Learning Curve | Low | Low | Medium-High |
| Community Size | Large (legacy) | Growing fast | Large |
| Best For | Publishing npm packages | SaaS apps, startups | Enterprise platforms |
| Configuration | lerna.json | turbo.json | nx.json + project.json |
| Package Manager | npm, yarn, pnpm | npm, yarn, pnpm | npm, yarn, pnpm |
The Hybrid Approach: Combining Tools
Many teams combine tools for the best experience:
// Use Turborepo for build orchestration + caching
// turbo.json
{
"pipeline": {
"build": { "dependsOn": ["^build"], "outputs": ["dist/**"] },
"test": { "dependsOn": ["build"] }
}
}
// Use Lerna for publishing
// lerna.json
{
"version": "independent",
"npmClient": "pnpm",
"useWorkspaces": true,
"command": {
"publish": {
"conventionalCommits": true
}
}
}This gives you Turborepo's fast builds with Lerna's publishing workflow. Alternatively, use Nx for everything if you need the full platform experience.
Real-World Case Studies
Vercel's Monorepo with Turborepo
Vercel manages their entire platform—including Next.js, the Vercel dashboard, and dozens of internal packages—in a single monorepo powered by Turborepo. Their setup uses pnpm workspaces for dependency management and Turborepo for build orchestration. Remote caching on Vercel's infrastructure means that CI builds that would take 45 minutes complete in under 5 minutes when most packages are unchanged. The key insight is that Turborepo's hash-based caching works across machines—a build cached on a developer's laptop is the same hash as the same build on CI, so cached artifacts are shared transparently.
Google's Approach with Nx
Google's JavaScript monorepo (one of the largest in the world) influenced Nx's design. Nrwl, the company behind Nx, was founded by former Google engineers who brought the same dependency graph analysis and affected-target detection patterns from Bazel. Companies like Cisco, FedEx, and Capital One use Nx to manage monorepos with hundreds of packages. Nx's computation cache can be shared across the entire organization via Nx Cloud, and its distributed task execution splits work across multiple CI agents automatically.
Shopify's Hybrid Approach
Shopify uses a combination of tools: their Ruby monorepo uses custom tooling, but their frontend JavaScript packages use a hybrid Turborepo plus changesets setup. Turborepo handles build orchestration and caching, while changesets (a lightweight alternative to Lerna's conventional commits) manages versioning and publishing. This demonstrates that monorepo tools are not mutually exclusive—you can combine the best features of different tools.
Decision Framework
Use this decision tree to choose the right tool:
- Small monorepo (< 10 packages), simple needs: Start with pnpm workspaces alone. You may not need a dedicated tool.
- Focus on build speed with minimal config: Choose Turborepo. It requires only a
turbo.jsonfile and works with any package manager. - Need code generation, linting rules, and project graph: Choose Nx. It provides a full development platform but has a steeper learning curve.
- Primary goal is npm package publishing: Use Lerna or changesets. Lerna excels at versioning and publishing workflows.
- Very large monorepo (100+ packages), enterprise team: Choose Nx with Nx Cloud for distributed execution and governance features.
- Want Turborepo's speed with Lerna's publishing: Combine them—use Turborepo for builds and Lerna for publishing.
Future Outlook
The monorepo tooling landscape is evolving rapidly. Turborepo is adding features like incremental builds, better pruning for Docker, and tighter Vercel integration. Nx is expanding beyond JavaScript with plugins for Go, Rust, and Java. Lerna, now maintained by the Nx team, continues to receive updates focused on caching and performance.
The trend toward remote caching and distributed execution will continue, making monorepo tooling more efficient and accessible. AI-assisted dependency analysis and build optimization are emerging trends that could further improve monorepo performance.
Conclusion
Choosing between Lerna, Turborepo, and Nx depends on your specific needs:
- Lerna is best for simple monorepos focused on package publishing with conventional commits
- Turborepo excels at build performance with minimal configuration and excellent Vercel integration
- Nx provides a comprehensive development platform for large teams needing code generation, governance, and distributed execution
Key takeaways:
- Start simple — Don't over-engineer from the beginning; Turborepo is usually the right first choice
- Cache aggressively — Remote caching is the single biggest performance improvement
- Build only what changed — Affected detection saves time and money
- Consider your team — Choose based on team size, expertise, and organizational complexity
- Plan for growth — Your tooling should scale with your monorepo; it's easier to add Nx later than to undo its complexity
Start with the simplest tool that meets your needs, and migrate to more powerful tools as your monorepo grows. Focus on build performance, caching, and affected detection regardless of which tool you choose.