Monorepo — The Complete Guide to Managing Multiple Projects in One Repository

Monorepo — The Complete Guide to Managing Multiple Projects in One Repository

12/14/2025 DevOps By Tech Writers
MonorepoDevOpsToolingNxTurborepoArchitectureDeveloper ExperienceCI/CD

Introduction: Why Monorepos Are Taking Over

Monorepos are increasingly popular in modern software development — and for good reason. Major companies like Google, Meta, Microsoft, and large open-source projects such as Babel, Jest, and React all use a monorepo approach. But what exactly is a monorepo, and why should you consider one for your team?

In this comprehensive guide, we’ll cover everything from the fundamentals to the tools that make monorepo development a delight.

Table of Contents

What is a Monorepo?

A monorepo is a single repository containing multiple distinct projects, with well-defined relationships between them.

The key phrase here is “well-defined relationships.” A monorepo is not just dumping all your code into a single repository. Each project within the monorepo should be a discrete unit with clear boundaries, dependencies, and ownership.

What a Monorepo Is Not

  • Not just “code colocation” — If you have several projects in a repo but no well-defined relationships, that’s just a folder with code, not a monorepo.
  • Not a monolith — If a repo contains a massive application without division into encapsulated parts, it’s just a big (monolithic) repo. A good monorepo is the opposite of monolithic.
# A well-structured monorepo
my-monorepo/
├── apps/
│   ├── web-app/          # React frontend
│   ├── mobile-app/       # React Native app
│   └── api-server/       # Node.js backend
├── packages/
│   ├── ui-components/    # Shared UI library
│   ├── utils/            # Shared utilities
│   └── config/           # Shared configs (ESLint, TypeScript, etc.)
├── package.json
├── nx.json               # or turbo.json
└── README.md

Monorepo Structure Diagram

graph TD
    A[Monorepo Root] --> B[apps/]
    A --> C[packages/]
    A --> D[tools/]
    A --> E[configs/]
    
    B --> F[web-app]
    B --> G[mobile-app]
    B --> H[api-server]
    
    C --> I[ui-components]
    C --> J[utils]
    C --> K[config-eslint]
    C --> L[config-typescript]
    
    F --> I
    F --> J
    G --> I
    G --> J
    H --> J
    H --> K
    
    style A fill:#e1f5fe
    style B fill:#f3e5f5
    style C fill:#e8f5e8
    style D fill:#fff3e0
    style E fill:#fce4ec

Monorepo vs. Monolith

This is one of the most common misconceptions. Let’s set the record straight:

AspectMonorepoMonolith
StructureMultiple distinct projects in one repoOne tightly-coupled application
BoundariesClear module boundaries and APIsBlurred boundaries between components
DeploymentIndependent deployment per projectSingle deployment unit
ScalabilityScales with proper toolingBecomes harder to scale
Team OwnershipEach team owns their projectsSingle team or shared ownership

A good monorepo actually promotes modularity by making it easy to share code while keeping projects independent and deployable on their own.

Polyrepo: The Traditional Approach

A polyrepo (the opposite of a monorepo) is the standard approach where each team, application, or library has its own repository. While polyrepos offer team autonomy, that autonomy comes through isolation, and isolation hurts collaboration.

Common Polyrepo Problems

1. Cumbersome Code Sharing

To share code across repositories, you need to create a separate repo for shared code, set up CI/CD, configure package publishing, and manage versioning. It’s a significant overhead.

# Polyrepo: sharing code is painful
team-a-repo/     → publishes @company/shared-utils v1.2.0
team-b-repo/     → depends on @company/shared-utils v1.1.0  😬
team-c-repo/     → depends on @company/shared-utils v1.0.0  😱

2. Significant Code Duplication

Because setting up shared repos is so tedious, teams often rewrite common utilities in each repo — wasting time and increasing maintenance burden.

3. Costly Cross-Repo Changes

A critical bug in a shared library? You’ll need to fix it in the library repo, publish a new version, then update and test every consumer repo separately. Coordinating versions across repos is a nightmare.

4. Inconsistent Tooling

Each project uses its own set of commands for testing, building, linting, and deploying. This creates mental overhead as developers switch between projects.

Why Choose a Monorepo?

A monorepo solves these polyrepo pain points elegantly:

Polyrepo vs Monorepo Workflow Comparison

flowchart TD
    subgraph "Polyrepo Workflow"
        A1[Fix shared library bug] --> B1[Update library in lib-repo]
        B1 --> C1[Publish new version v2.1.0]
        C1 --> D1[Update consumer-repo-1 to v2.1.0]
        C1 --> E1[Update consumer-repo-2 to v2.1.0]
        C1 --> F1[Update consumer-repo-3 to v2.1.0]
        D1 --> G1[Test consumer-repo-1]
        E1 --> H1[Test consumer-repo-2]
        F1 --> I1[Test consumer-repo-3]
        G1 --> J1[Deploy consumer-repo-1]
        H1 --> K1[Deploy consumer-repo-2]
        I1 --> L1[Deploy consumer-repo-3]
    end
    
    subgraph "Monorepo Workflow"
        A2[Fix shared library bug] --> B2[Update library in packages/]
        B2 --> C2[Commit all changes together]
        C2 --> D2[Run affected tests]
        D2 --> E2[Deploy all affected apps]
    end
    
    style A1 fill:#ffebee
    style A2 fill:#e8f5e8
    style B1 fill:#ffebee
    style B2 fill:#e8f5e8

No Overhead to Create New Projects

Use the existing CI setup. No need to publish versioned packages when all consumers are in the same repo.

# With Nx: create a new app in seconds
npx nx g @nx/react:app my-new-app

# With Turborepo: just add a new folder
mkdir apps/my-new-app

Atomic Commits Across Projects

Everything works together at every commit. Fix a shared library and all its consumers in the same commit — no breaking changes, no version coordination.

# One commit that updates shared lib + all consumers
git commit -m "fix: update validation logic across all apps"

One Version of Everything

No more incompatible versions of third-party libraries across projects. A single package.json (or a root dependency manager) ensures consistency.

Developer Mobility

Developers get a consistent way of building and testing across different tools and technologies. They can confidently contribute to other teams’ projects and verify their changes are safe.

Key Features of a Monorepo

When choosing a monorepo tool, here are the features to look for:

⚡ Speed (Fast)

Local Computation Caching

Store and replay file and process outputs. Never build or test the same thing twice on the same machine.

# First run: builds everything
npx nx build my-app    # Takes 45 seconds

# Second run: restored from cache
npx nx build my-app    # Takes 0.5 seconds ⚡

Local Task Orchestration

Run tasks in the correct order and in parallel, respecting the dependency graph.

# Nx runs tasks in parallel, respecting dependencies
npx nx run-many --target=build --all --parallel=5

Distributed Computation Caching

Share cache artifacts across your entire organization, including CI agents. No one in your team ever builds or tests the same thing twice.

Distributed Task Execution

Distribute a single command across many CI machines while maintaining the developer experience of running it locally.

Detecting Affected Projects

Only build and test what’s affected by a change, rather than the entire repo.

# Only test projects affected by changes since main
npx nx affected --target=test --base=main

# Turborepo equivalent
npx turbo run test --filter=...[origin/main]

🧠 Understandable

Workspace Analysis

Understand the project graph of the workspace automatically by analyzing package.json, source files, and configuration.

Dependency Graph Visualization

Interactively visualize dependency relationships between projects and tasks.

graph TD
    subgraph "Apps"
        A[web-app] 
        B[mobile-app]
        C[api-server]
    end
    
    subgraph "UI Libraries"
        D[ui-button]
        E[ui-form]
        F[ui-modal]
    end
    
    subgraph "Shared Libraries"
        G[utils-date]
        H[utils-string]
        I[config-eslint]
        J[config-typescript]
    end
    
    A --> D
    A --> E
    A --> F
    A --> G
    A --> H
    
    B --> D
    B --> E
    B --> G
    B --> H
    
    C --> G
    C --> H
    C --> I
    C --> J
    
    D --> G
    E --> G
    F --> H
    
    style A fill:#e3f2fd
    style B fill:#f3e5f5
    style C fill:#e8f5e8
    style D fill:#fff3e0
    style E fill:#fff3e0
    style F fill:#fff3e0
    style G fill:#fce4ec
    style H fill:#fce4ec
    style I fill:#e1f5fe
    style J fill:#e1f5fe
# Nx: open interactive graph visualization
npx nx graph

# Turborepo: generate graph
npx turbo run build --graph

🔧 Manageable

Source Code Sharing

Easily share discrete pieces of source code between projects without publishing packages.

Consistent Tooling

Get a consistent experience regardless of what technology each project uses — JavaScript, TypeScript, Go, Rust, Java, etc.

Code Generation

Scaffold new projects, components, and modules with built-in generators.

# Nx: generate a new React component
npx nx g @nx/react:component my-button --project=ui-lib

# Nx: generate a new Node.js library
npx nx g @nx/node:lib shared-utils

Project Constraints and Visibility

Define rules to constrain dependency relationships. Mark projects as private, separate frontend from backend, and enforce architectural boundaries.

// nx.json - enforce module boundaries
{
  "targetDefaults": {},
  "plugins": [
    {
      "plugin": "@nx/eslint/plugin",
      "options": {
        "targetName": "lint"
      }
    }
  ]
}

Monorepo Tools Compared

Here’s how the major monorepo tools stack up:

Nx

“Nx optimizes your builds, scales your CI, and fixes failed PRs.”

  • Best for: Full-featured monorepo management with excellent DX
  • Language support: JavaScript/TypeScript, plus plugins for Go, Rust, Java, etc.
  • Highlights: Interactive graph visualizer, powerful code generators, distributed task execution, first-class MCP server support
  • Cache: Local + remote (via Nx Cloud)

Turborepo (by Vercel)

“The high-performance build system for JavaScript & TypeScript codebases.”

  • Best for: Simple, fast task running in JS/TS monorepos
  • Language support: JavaScript/TypeScript
  • Highlights: Zero-config caching, incremental builds, remote caching via Vercel
  • Cache: Local + remote

Bazel (by Google)

“A fast, scalable, multi-language and extensible build system.”

  • Best for: Massive repositories (billions of lines of code), polyglot projects
  • Language support: Almost any language via build rules
  • Highlights: Transparent remote execution, most mature distributed execution
  • Cache: Local + remote

Lerna (maintained by Nx team)

“A tool for managing JavaScript projects with multiple packages.”

  • Best for: npm package publishing from monorepos
  • Language support: JavaScript/TypeScript
  • Highlights: Versioning and publishing workflows, powered by Nx under the hood since v6
  • Cache: Local + remote (via Nx Cloud)

moon (by moonrepo)

“A task runner and monorepo management tool for the web ecosystem.”

  • Best for: Web projects wanting an integrated toolchain
  • Language support: JavaScript/TypeScript, plus tier 2 language support
  • Highlights: Built-in toolchain management, native code generation, project constraints

Pants (by Pants Build)

“A fast, scalable, user-friendly build system for codebases of all sizes.”

  • Best for: Polyglot repos, especially Python + backend languages
  • Language support: Python, Java, Scala, Go, Shell, Docker
  • Highlights: Automatic dependency inference via static analysis, transparent remote execution

Rush (by Microsoft)

“Geared for large monorepos with lots of teams and projects.”

  • Best for: Large enterprise JavaScript/TypeScript monorepos
  • Language support: JavaScript/TypeScript
  • Highlights: NPM dependency approvals, version policies, BuildXL integration

Feature Comparison Matrix

FeatureNxTurborepoBazelLernamoonPantsRush
Local caching
Task orchestration
Remote caching
Distributed execution🔧
Affected detection🔧
Graph visualization🔧🔧
Consistent tooling
Code generation🔧🔧🔧🔧
Project constraints🔧🔧🔧

✅ = Natively supported | 🔧 = Implement your own | ❌ = Not supported

Choosing the Right Tool

Here’s a quick decision guide:

  • Small-to-medium JS/TS project, want simplicityTurborepo
  • Full-featured monorepo with great DXNx
  • Massive polyglot codebase (Google-scale)Bazel
  • Need to publish npm packagesLerna (with Nx)
  • Web ecosystem with integrated toolingmoon
  • Python/backend-heavy polyglot repoPants
  • Large enterprise JS/TS with strict governanceRush

Getting Started: Monorepo with Nx

Here’s how to set up a monorepo with Nx:

# Create a new Nx workspace
npx create-nx-workspace@latest my-monorepo

# Choose your preset (React, Angular, Node, etc.)
# or start with an empty workspace

cd my-monorepo

Adding Projects

# Add a React application
npx nx g @nx/react:app web-app

# Add a Node.js API
npx nx g @nx/node:app api

# Add a shared library
npx nx g @nx/react:lib ui-components

Running Tasks

# Build a specific project
npx nx build web-app

# Run tests for affected projects
npx nx affected --target=test

# Visualize the dependency graph
npx nx graph

# Run multiple tasks in parallel
npx nx run-many --target=build --all --parallel=5

Nx Configuration

// nx.json
{
  "targetDefaults": {
    "build": {
      "dependsOn": ["^build"],
      "cache": true
    },
    "test": {
      "cache": true
    },
    "lint": {
      "cache": true
    }
  },
  "defaultBase": "main"
}

Getting Started: Monorepo with Turborepo

# Create a new Turborepo
npx create-turbo@latest my-turborepo

cd my-turborepo

Project Structure

my-turborepo/
├── apps/
│   ├── web/              # Next.js app
│   └── docs/             # Documentation site
├── packages/
│   ├── ui/               # Shared UI components
│   ├── eslint-config/    # Shared ESLint config
│   └── typescript-config/ # Shared TS config
├── turbo.json
└── package.json

Turborepo Configuration

// turbo.json
{
  "$schema": "https://turbo.build/schema.json",
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**", ".next/**"]
    },
    "test": {
      "dependsOn": ["build"]
    },
    "lint": {},
    "dev": {
      "cache": false,
      "persistent": true
    }
  }
}

Running Tasks

# Build all projects
npx turbo run build

# Run dev servers
npx turbo run dev

# Only run tasks for specific packages
npx turbo run build --filter=web

# Run affected tasks
npx turbo run test --filter=...[origin/main]

Best Practices

1. Define Clear Project Boundaries

Organize your monorepo with clear boundaries between apps and libraries:

monorepo/
├── apps/          # Deployable applications
├── packages/      # Shared libraries
├── tools/         # Build tools and scripts
└── configs/       # Shared configurations

2. Use Module Boundaries

Enforce that backend projects don’t import from frontend projects and vice versa. Tools like Nx provide built-in lint rules for this.

// .eslintrc.json with Nx module boundary rules
{
  "rules": {
    "@nx/enforce-module-boundaries": [
      "error",
      {
        "depConstraints": [
          { "sourceTag": "scope:frontend", "onlyDependOnLibsWithTags": ["scope:shared"] },
          { "sourceTag": "scope:backend", "onlyDependOnLibsWithTags": ["scope:shared"] }
        ]
      }
    ]
  }
}

3. Cache Aggressively

Enable both local and remote caching. This can reduce CI times by 50-90%.

4. Use Affected Commands in CI

Never build and test everything — only what’s affected by the current change.

# GitHub Actions example
- name: Test affected
  run: npx nx affected --target=test --base=origin/main

5. Keep Shared Libraries Small and Focused

Don’t create a single “shared” library with everything. Instead, create small, focused libraries:

packages/
├── ui-button/
├── ui-form/
├── utils-date/
├── utils-string/
└── config-eslint/

6. Automate Dependency Updates

Use tools like Renovate or Dependabot to keep dependencies in sync across the entire monorepo.

7. Standardize Development Commands

Ensure every project uses the same commands for common tasks:

nx build <project>
nx test <project>
nx lint <project>
nx serve <project>

Conclusion

A monorepo is more than a code organization strategy — it’s a shift in how your organization collaborates and ships software. By consolidating projects into a single repository with well-defined relationships, you gain:

  • Faster development through code sharing and caching
  • Better collaboration with atomic commits and consistent tooling
  • Improved code quality through enforced module boundaries
  • Simplified CI/CD with affected commands and distributed execution

The tooling ecosystem has matured significantly, with options for every team size and tech stack. Whether you choose Nx for its comprehensive features, Turborepo for its simplicity, or Bazel for its scale, the key is to start small and grow your monorepo practices incrementally.

Remember: a monorepo is not a monolith. Done right, it’s the opposite — a modular, scalable, and collaborative way to manage your codebase.

Further Resources