MinhVo

Minh Vo

rss feed

Slaying code & making it lit fr fr 🔥 tagline

Hey there 👋 I'm an AI Engineer with 7 years of experience building scalable web and mobile applications. Currently at Neurond AI (May 2025 — present), architecting an Enterprise AI Assistant Platform with multi-tenant RAG on pgvector, multi-provider LLM orchestration, and Azure-native infrastructure. Previously spent 5+ years at SNAPTEC (Sep 2019 — Apr 2025), leading SaaS themes, admin dashboards, and e-commerce platforms — earned the Hero of the Year award in 2021. I specialize in TypeScript, React, Next.js, and AI-Native engineering with Claude Code and Cursor.bio

Back to blogs

Monorepo Tools: Turborepo, Nx, and Lerna Compared

Compare monorepo tools: Turborepo, Nx, and Lerna — features, performance, and use cases.

MonorepoTurborepoNxDevOps

By MinhVo

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.

Monorepo Tools Comparison

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:

ChallengeWithout ToolingWith Tooling
Dependency ManagementManual workspace configurationAutomatic workspace linking
Build OrchestrationRun all builds sequentiallyDependency-aware parallel builds
CachingRebuild everythingCache and skip unchanged tasks
Package PublishingManual versioning and publishingAutomated versioning and publishing
Code SharingComplex import pathsSimple workspace references
ConsistencyInconsistent configurationsShared tooling and configs
Affected DetectionRun all tests alwaysOnly 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"]
      }
    }
  }
}

Tool Architecture

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-commits

Lerna'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 graph

Nx'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-plugin

Distributed 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/main

Build Performance

Performance Comparison

Performance is often the deciding factor. Here's how the three tools compare on a typical monorepo with 50 packages:

MetricLernaTurborepoNx
Cold build (50 packages)45 min25 min20 min
Cached build (50 packages)45 min2 min2 min
Affected build (3 packages changed)15 min5 min4 min
Remote cache hitN/A30 sec30 sec
Parallel executionBasicAdvancedAdvanced
Distributed executionNoNoYes (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/main

Real-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 test

From 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-commits

From 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 caching

Best Practices

  1. Start simple, scale gradually: Begin with Turborepo for small monorepos. Migrate to Nx when you need code generation or distributed execution.

  2. Enable remote caching immediately: This is the single highest-impact optimization. Both Turborepo and Nx Cloud offer free tiers for small teams.

  3. Use affected detection religiously: Never run full builds on PRs. Use --filter='...[origin/main]' (Turborepo) or nx affected (Nx) to only build what changed.

  4. Standardize tooling across packages: Use shared ESLint, Prettier, and TypeScript configurations. Both Turborepo and Nx provide starter templates with shared configs.

  5. Automate publishing with conventional commits: Use Lerna or Changesets for automated versioning. Write meaningful commit messages so version bumps are automatic.

  6. 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.

  7. 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.

  8. Document package boundaries: Use CODEOWNERS for review requirements. With Nx, use lint rules to enforce module boundaries.

Common Pitfalls

PitfallImpactSolution
No remote cachingSlow CI, redundant buildsEnable Turborepo or Nx Cloud immediately
Running full builds on PRsWasted CI resources, slow feedbackUse affected detection
Inconsistent configurationsBuild failures, style driftUse shared configs (eslint-config, tsconfig)
Circular dependenciesBuild failures, slow analysisUse Nx module boundary rules
Missing CI for shared packagesBreaking changes go undetectedAlways test affected dependents
Over-engineering with NxUnnecessary complexity for small projectsStart with Turborepo, add Nx when needed
Lerna without cachingNo performance benefit over manual scriptsAdd Nx-powered caching (Lerna 7+)

Comparison Matrix

FeatureLernaTurborepoNx
Primary FocusPackage publishingBuild performanceDevelopment platform
Remote CachingVia Nx integrationVercel Remote CacheNx Cloud
Affected DetectionBasic (--since)Advanced (--filter)Advanced (nx affected)
Code GenerationNoNoYes (generators)
Dependency GraphBasicAdvancedAdvanced + visual
Distributed ExecutionNoNoYes (Nx Cloud)
Learning CurveLowLowMedium-High
Community SizeLarge (legacy)Growing fastLarge
Best ForPublishing npm packagesSaaS apps, startupsEnterprise platforms
Configurationlerna.jsonturbo.jsonnx.json + project.json
Package Managernpm, yarn, pnpmnpm, yarn, pnpmnpm, 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.json file 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:

  1. Lerna is best for simple monorepos focused on package publishing with conventional commits
  2. Turborepo excels at build performance with minimal configuration and excellent Vercel integration
  3. 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.