A monorepo isn’t just a way to organize code; it’s a powerful strategy for managing a complex web of interconnected projects by treating them as a single unit, unlocking efficiency gains you’d typically only find in smaller, single-project setups.

Let’s see this in action. Imagine you have a few related React applications and shared UI components. Instead of separate Git repositories and complex cross-repo versioning, a monorepo brings them together.

Here’s a simplified package.json in the root of our monorepo:

{
  "name": "my-company-monorepo",
  "version": "1.0.0",
  "private": true,
  "scripts": {
    "dev": "turbo run dev",
    "build": "turbo run build",
    "lint": "turbo run lint"
  },
  "devDependencies": {
    "turbo": "^1.10.3",
    "eslint": "^8.45.0",
    "tsconfig-paths": "^4.2.0"
  },
  "packageManager": "pnpm@8.6.0"
}

And here’s what our turbo.json might look like:

{
  "$schema": "https://turborepo.org/schema.json",
  "pipeline": {
    "build": {
      "outputs": ["dist/**", ".next/**"],
      "dependsOn": ["^build"]
    },
    "lint": {
      "outputs": []
    },
    "dev": {
      "cache": false
    }
  }
}

Inside the apps/ directory, we might have web-app and admin-app. In packages/, we could have ui-components and shared-utils. Each of these is its own npm/pnpm/yarn package with its own package.json and build scripts.

The core problem this solves is dependency management and build orchestration across multiple, often interdependent, projects. Without a monorepo, updating a shared library means coordinating releases and updates across many repositories. With it, you can make a change in packages/ui-components, run turbo run build from the root, and see that change reflected in apps/web-app and apps/admin-app immediately, without publishing anything.

The magic happens through tools like Turborepo or Nx. They understand the dependency graph of your packages. When you run turbo run build, Turborepo analyzes which packages depend on each other. If web-app depends on ui-components, Turborepo will ensure ui-components is built before web-app. It also caches build artifacts, so if ui-components hasn’t changed, its build output is reused, making subsequent builds incredibly fast. The dependsOn field in turbo.json is key here; ^build means "build all packages that this package depends on."

The packageManager field in the root package.json (here, pnpm@8.6.0) is crucial for ensuring consistent dependency installation across all workspaces. pnpm is often favored in monorepos for its efficient disk space usage via hard-linking and content-addressable store.

Consider the build pipeline in turbo.json. The outputs field tells Turborepo where to find the build artifacts for each package (e.g., dist/** for a typical Node.js package or .next/** for a Next.js app). This allows Turborepo to cache and serve these outputs effectively. The cache: false for dev means that development servers won’t be cached, ensuring you always get live updates.

A common pitfall is not explicitly defining package dependencies. If web-app uses ui-components, its package.json must list ui-components as a dependency. For example, in apps/web-app/package.json:

{
  "name": "web-app",
  "version": "0.1.0",
  "dependencies": {
    "react": "^18.2.0",
    "ui-components": "workspace:*" // Or specific version if needed
  },
  // ... other fields
}

The workspace:* syntax (used by npm, yarn, and pnpm) tells the package manager to link to the local version of ui-components within the monorepo, not an external npm registry.

The most surprising mechanical detail is how Turborepo’s caching works. It hashes the contents of your source files and your package.json dependencies. If these hashes match between runs, it reuses the cached output without executing the build script. This means even if you have a complex build process involving multiple steps, if nothing relevant has changed, the build command might not run at all, saving immense time.

The next logical step is exploring how to manage external dependencies across the monorepo, particularly ensuring consistent versions and optimizing installation.

Want structured learning?

Take the full React course →