JavaScript

Monorepos with Nx or TurboRepo: when and why

Monorepos With Nx Or TurboRepo When And Why

Introduction

As teams grow and codebases expand, managing multiple repositories becomes increasingly difficult. Dependencies drift apart, tooling configurations diverge, and coordinating changes across repositories requires significant overhead. Monorepos solve these problems by keeping related projects in a single repository, enabling atomic commits, shared tooling, and consistent dependency management. Tools like Nx and Turborepo make monorepos fast, scalable, and developer-friendly by providing intelligent caching, task orchestration, and dependency graph analysis. In this comprehensive guide, you will learn when a monorepo makes sense, how to set up both Nx and Turborepo, and which tool fits your team best based on real-world scenarios and practical code examples.

Why Teams Choose a Monorepo

Before adopting a monorepo, it helps to understand the tangible benefits that organizations like Google, Microsoft, Meta, and many startups have discovered through years of experience.

Shared code is easier to reuse without publishing packages
Dependencies stay consistent across all projects
Refactors span projects safely with atomic commits
Tooling and scripts are unified reducing configuration drift
CI pipelines become simpler with single source of truth
Code review improves when changes are visible in context
Onboarding accelerates with everything in one place

As a result, teams move faster, reduce long-term maintenance costs, and eliminate the coordination overhead of managing multiple repositories.

Common Monorepo Challenges

However, monorepos also introduce risks if not managed well. Without proper tooling, the benefits quickly turn into liabilities.

Slow builds without proper caching and affected detection
Complex dependency graphs that become hard to understand
Large pull requests that touch many projects
Tooling complexity requiring specialized knowledge
CI performance issues running all tests for every change
Git performance degradation with large histories
IDE slowdowns indexing massive codebases

This is where Nx and Turborepo add real value. They solve the scale problem while preserving the benefits.

What Is Nx

Nx is a powerful monorepo platform with deep tooling, strong project awareness, and comprehensive plugin ecosystem. It works exceptionally well for large teams and complex systems that need structure and governance.

Core Features of Nx

Smart task scheduling with parallel execution
Dependency graph visualization for understanding relationships
Affected-only builds and tests based on git diff
Built-in code generators for consistent project creation
Strong TypeScript support with type-checking across projects
Plugin ecosystem for React, Angular, Node, and more
Nx Cloud for distributed caching and task execution

Setting Up Nx Workspace

# Create a new Nx workspace
npx create-nx-workspace@latest my-workspace --preset=ts

# Interactive prompts will guide you through:
# - Choose a preset (apps, ts, react, angular, node, etc.)
# - Enable Nx Cloud for distributed caching
# - Choose package manager (npm, yarn, pnpm)

cd my-workspace

Nx Workspace Structure

my-workspace/
├── apps/                    # Application projects
│   ├── web-app/
│   │   ├── src/
│   │   └── project.json
│   └── api/
│       ├── src/
│       └── project.json
├── libs/                    # Shared libraries
│   ├── shared/
│   │   └── ui/
│   │       ├── src/
│   │       └── project.json
│   └── feature/
│       └── auth/
│           ├── src/
│           └── project.json
├── tools/                   # Custom tooling and generators
├── nx.json                  # Nx configuration
├── tsconfig.base.json       # Base TypeScript config
└── package.json

nx.json Configuration

// nx.json
{
  "$schema": "./node_modules/nx/schemas/nx-schema.json",
  "namedInputs": {
    "default": ["{projectRoot}/**/*", "sharedGlobals"],
    "production": [
      "default",
      "!{projectRoot}/**/?(*.)+(spec|test).[jt]s?(x)?(.snap)",
      "!{projectRoot}/tsconfig.spec.json",
      "!{projectRoot}/.eslintrc.json"
    ],
    "sharedGlobals": ["{workspaceRoot}/tsconfig.base.json"]
  },
  "targetDefaults": {
    "build": {
      "dependsOn": ["^build"],
      "inputs": ["production", "^production"],
      "cache": true
    },
    "test": {
      "inputs": ["default", "^production", "{workspaceRoot}/jest.preset.js"],
      "cache": true
    },
    "lint": {
      "inputs": ["default", "{workspaceRoot}/.eslintrc.json"],
      "cache": true
    },
    "e2e": {
      "inputs": ["default", "^production"],
      "cache": true
    }
  },
  "parallel": 3,
  "cacheDirectory": "node_modules/.cache/nx",
  "defaultBase": "main"
}

project.json Configuration

// apps/web-app/project.json
{
  "name": "web-app",
  "$schema": "../../node_modules/nx/schemas/project-schema.json",
  "projectType": "application",
  "sourceRoot": "apps/web-app/src",
  "tags": ["scope:web", "type:app"],
  "targets": {
    "build": {
      "executor": "@nx/vite:build",
      "outputs": ["{options.outputPath}"],
      "options": {
        "outputPath": "dist/apps/web-app"
      },
      "configurations": {
        "production": {
          "mode": "production"
        },
        "development": {
          "mode": "development"
        }
      },
      "defaultConfiguration": "production"
    },
    "serve": {
      "executor": "@nx/vite:dev-server",
      "options": {
        "buildTarget": "web-app:build",
        "port": 4200
      },
      "configurations": {
        "production": {
          "buildTarget": "web-app:build:production"
        },
        "development": {
          "buildTarget": "web-app:build:development"
        }
      },
      "defaultConfiguration": "development"
    },
    "test": {
      "executor": "@nx/vite:test",
      "outputs": ["{options.reportsDirectory}"],
      "options": {
        "passWithNoTests": true,
        "reportsDirectory": "../../coverage/apps/web-app"
      }
    },
    "lint": {
      "executor": "@nx/eslint:lint",
      "options": {
        "lintFilePatterns": ["apps/web-app/**/*.{ts,tsx,js,jsx}"]
      }
    }
  }
}

Creating Libraries with Nx

# Generate a new React library
npx nx g @nx/react:library ui --directory=libs/shared/ui

# Generate a Node.js library
npx nx g @nx/node:library utils --directory=libs/shared/utils

# Generate with specific tags for dependency constraints
npx nx g @nx/react:library auth --directory=libs/feature/auth --tags="scope:shared,type:feature"

Nx Dependency Constraints

Enforce architectural boundaries using module boundary rules.

// .eslintrc.json (root)
{
  "root": true,
  "plugins": ["@nx"],
  "overrides": [
    {
      "files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
      "rules": {
        "@nx/enforce-module-boundaries": [
          "error",
          {
            "enforceBuildableLibDependency": true,
            "allow": [],
            "depConstraints": [
              {
                "sourceTag": "type:app",
                "onlyDependOnLibsWithTags": ["type:feature", "type:ui", "type:util"]
              },
              {
                "sourceTag": "type:feature",
                "onlyDependOnLibsWithTags": ["type:ui", "type:util", "type:data-access"]
              },
              {
                "sourceTag": "type:ui",
                "onlyDependOnLibsWithTags": ["type:ui", "type:util"]
              },
              {
                "sourceTag": "type:util",
                "onlyDependOnLibsWithTags": ["type:util"]
              },
              {
                "sourceTag": "scope:web",
                "onlyDependOnLibsWithTags": ["scope:web", "scope:shared"]
              },
              {
                "sourceTag": "scope:api",
                "onlyDependOnLibsWithTags": ["scope:api", "scope:shared"]
              }
            ]
          }
        ]
      }
    }
  ]
}

Running Nx Commands

# Run a single project target
npx nx build web-app
npx nx test shared-ui
npx nx lint api

# Run affected projects only (based on git diff)
npx nx affected:build
npx nx affected:test
npx nx affected:lint

# Run for all projects
npx nx run-many -t build
npx nx run-many -t test --parallel=5

# Visualize dependency graph
npx nx graph

# Show what would be affected by current changes
npx nx affected:graph

What Is Turborepo

Turborepo focuses on speed and simplicity. It works on top of existing package managers and adds powerful caching without requiring major restructuring of your codebase.

Core Features of Turborepo

Incremental builds only rebuilding what changed
Local and remote caching via Vercel or self-hosted
Simple configuration with turbo.json
Works with npm, pnpm, and yarn workspaces
Minimal learning curve for teams familiar with npm scripts
Parallel execution with intelligent scheduling
Zero runtime – just a task runner

Setting Up Turborepo

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

# Or add Turborepo to existing workspace
cd existing-monorepo
npm install turbo --save-dev

Turborepo Workspace Structure

my-turborepo/
├── apps/                    # Application packages
│   ├── web/
│   │   ├── src/
│   │   ├── package.json
│   │   └── tsconfig.json
│   └── api/
│       ├── src/
│       ├── package.json
│       └── tsconfig.json
├── packages/                # Shared packages
│   ├── ui/
│   │   ├── src/
│   │   ├── package.json
│   │   └── tsconfig.json
│   ├── config-eslint/
│   │   ├── index.js
│   │   └── package.json
│   └── config-typescript/
│       ├── base.json
│       └── package.json
├── turbo.json               # Turborepo configuration
├── package.json             # Root package.json
└── pnpm-workspace.yaml      # Workspace definition (for pnpm)

Root package.json

// package.json
{
  "name": "my-turborepo",
  "private": true,
  "scripts": {
    "build": "turbo run build",
    "dev": "turbo run dev",
    "lint": "turbo run lint",
    "test": "turbo run test",
    "clean": "turbo run clean && rm -rf node_modules",
    "format": "prettier --write \"**/*.{ts,tsx,md}\""
  },
  "devDependencies": {
    "prettier": "^3.2.0",
    "turbo": "^2.0.0"
  },
  "packageManager": "pnpm@8.15.0",
  "workspaces": [
    "apps/*",
    "packages/*"
  ]
}

turbo.json Configuration

// turbo.json
{
  "$schema": "https://turbo.build/schema.json",
  "globalDependencies": ["**/.env.*local"],
  "globalEnv": ["NODE_ENV"],
  "pipeline": {
    "build": {
      "dependsOn": ["^build"],
      "inputs": ["$TURBO_DEFAULT$", ".env*"],
      "outputs": ["dist/**", ".next/**", "!.next/cache/**"]
    },
    "lint": {
      "dependsOn": ["^build"],
      "inputs": ["$TURBO_DEFAULT$", ".eslintrc.js"]
    },
    "test": {
      "dependsOn": ["build"],
      "inputs": ["$TURBO_DEFAULT$", "**/*.test.ts", "**/*.test.tsx"],
      "outputs": ["coverage/**"]
    },
    "dev": {
      "cache": false,
      "persistent": true
    },
    "clean": {
      "cache": false
    }
  }
}

Package-Specific Configuration

// apps/web/package.json
{
  "name": "@myorg/web",
  "version": "0.0.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint",
    "test": "vitest run"
  },
  "dependencies": {
    "@myorg/ui": "workspace:*",
    "next": "^14.0.0",
    "react": "^18.2.0",
    "react-dom": "^18.2.0"
  },
  "devDependencies": {
    "@myorg/config-eslint": "workspace:*",
    "@myorg/config-typescript": "workspace:*",
    "@types/node": "^20.0.0",
    "@types/react": "^18.2.0",
    "typescript": "^5.3.0",
    "vitest": "^1.0.0"
  }
}
// packages/ui/package.json
{
  "name": "@myorg/ui",
  "version": "0.0.0",
  "private": true,
  "main": "./dist/index.js",
  "module": "./dist/index.mjs",
  "types": "./dist/index.d.ts",
  "exports": {
    ".": {
      "import": "./dist/index.mjs",
      "require": "./dist/index.js",
      "types": "./dist/index.d.ts"
    }
  },
  "scripts": {
    "build": "tsup src/index.tsx --format esm,cjs --dts",
    "dev": "tsup src/index.tsx --format esm,cjs --dts --watch",
    "lint": "eslint src/",
    "clean": "rm -rf dist"
  },
  "dependencies": {
    "react": "^18.2.0"
  },
  "devDependencies": {
    "@myorg/config-eslint": "workspace:*",
    "@myorg/config-typescript": "workspace:*",
    "@types/react": "^18.2.0",
    "tsup": "^8.0.0",
    "typescript": "^5.3.0"
  }
}

Shared UI Component Example

// packages/ui/src/index.tsx
import * as React from "react";

export interface ButtonProps {
  children: React.ReactNode;
  variant?: "primary" | "secondary" | "danger";
  size?: "sm" | "md" | "lg";
  disabled?: boolean;
  onClick?: () => void;
}

export function Button({
  children,
  variant = "primary",
  size = "md",
  disabled = false,
  onClick
}: ButtonProps) {
  const baseStyles = "rounded font-medium transition-colors focus:outline-none focus:ring-2";
  
  const variantStyles = {
    primary: "bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500",
    secondary: "bg-gray-200 text-gray-800 hover:bg-gray-300 focus:ring-gray-500",
    danger: "bg-red-600 text-white hover:bg-red-700 focus:ring-red-500"
  };
  
  const sizeStyles = {
    sm: "px-3 py-1.5 text-sm",
    md: "px-4 py-2 text-base",
    lg: "px-6 py-3 text-lg"
  };

  return (
    
  );
}

export interface CardProps {
  children: React.ReactNode;
  title?: string;
  className?: string;
}

export function Card({ children, title, className = "" }: CardProps) {
  return (
    
{title &&

{title}

} {children}
); }

Running Turborepo Commands

# Run build for all packages
pnpm turbo run build

# Run dev mode for all packages (parallel, no caching)
pnpm turbo run dev

# Run specific packages
pnpm turbo run build --filter=@myorg/web
pnpm turbo run build --filter=@myorg/ui

# Run affected packages (based on git)
pnpm turbo run build --filter=...[HEAD~1]

# Run with specific concurrency
pnpm turbo run build --concurrency=10

# Show dependency graph
pnpm turbo run build --graph

# Dry run to see what would execute
pnpm turbo run build --dry-run

Task Execution and Caching

Both tools optimize task execution through intelligent caching, but they approach it differently.

Nx Caching Mechanism

Nx uses computation hashing to determine if a task needs to run.

# First run - executes build
$ npx nx build web-app

> nx run web-app:build
Building...
Done in 45s

# Second run - uses cache
$ npx nx build web-app

> nx run web-app:build  [existing outputs match the cache, left as is]
Done in 0.5s

Nx computes a hash based on:

• Source files in the project
• Dependencies in the dependency graph
• Environment variables
• Runtime configuration
• External dependencies versions

Turborepo Caching Mechanism

Turborepo uses similar hash-based caching with configurable inputs and outputs.

# First run
$ pnpm turbo run build

• Packages in scope: @myorg/ui, @myorg/web, @myorg/api
• Running build in 3 packages
• Remote caching disabled

@myorg/ui:build: cache miss, executing
@myorg/web:build: cache miss, executing
@myorg/api:build: cache miss, executing

 Tasks:    3 successful, 3 total
 Cached:   0 cached, 3 total
 Time:     42.5s

# Second run
$ pnpm turbo run build

@myorg/ui:build: cache hit, replaying output
@myorg/web:build: cache hit, replaying output
@myorg/api:build: cache hit, replaying output

 Tasks:    3 successful, 3 total
 Cached:   3 cached, 3 total
 Time:     0.8s

Remote Caching

Both tools support remote caching to share build artifacts across team members and CI.

# Nx Cloud setup
npx nx connect-to-nx-cloud

# Turborepo with Vercel
npx turbo login
npx turbo link

CI/CD Integration

Monorepos shine when CI pipelines are optimized to only run affected tasks.

Nx GitHub Actions Workflow

# .github/workflows/ci.yml (Nx)
name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  main:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - uses: pnpm/action-setup@v2
        with:
          version: 8

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'pnpm'

      - name: Install dependencies
        run: pnpm install --frozen-lockfile

      - name: Set NX_BASE and NX_HEAD
        uses: nrwl/nx-set-shas@v4

      - name: Run affected lint
        run: pnpm nx affected:lint --base=${{ env.NX_BASE }} --head=${{ env.NX_HEAD }}

      - name: Run affected test
        run: pnpm nx affected:test --base=${{ env.NX_BASE }} --head=${{ env.NX_HEAD }}

      - name: Run affected build
        run: pnpm nx affected:build --base=${{ env.NX_BASE }} --head=${{ env.NX_HEAD }}

Turborepo GitHub Actions Workflow

# .github/workflows/ci.yml (Turborepo)
name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

env:
  TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
  TURBO_TEAM: ${{ vars.TURBO_TEAM }}

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 2

      - uses: pnpm/action-setup@v2
        with:
          version: 8

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'pnpm'

      - name: Install dependencies
        run: pnpm install --frozen-lockfile

      - name: Lint
        run: pnpm turbo run lint

      - name: Type check
        run: pnpm turbo run typecheck

      - name: Test
        run: pnpm turbo run test

      - name: Build
        run: pnpm turbo run build

Nx vs Turborepo: Feature Comparison

| Feature                    | Nx                      | Turborepo               |
|----------------------------|-------------------------|-------------------------|
| Task caching               | ✅ Local + Cloud         | ✅ Local + Vercel        |
| Affected detection         | ✅ Git-based             | ✅ Git-based             |
| Dependency graph           | ✅ Interactive UI        | ✅ GraphViz output       |
| Code generation            | ✅ Built-in generators   | ❌ Use external tools    |
| Module boundaries          | ✅ ESLint rules          | ❌ Manual enforcement    |
| Plugin ecosystem           | ✅ Rich ecosystem        | ❌ Minimal               |
| Framework support          | ✅ Deep integration      | ✅ Framework agnostic    |
| Configuration complexity   | Higher                  | Lower                   |
| Learning curve             | Steeper                 | Gentler                 |
| Migration from existing    | More restructuring      | Minimal changes         |
| Remote execution           | ✅ Nx Agents             | ❌ Caching only          |

When to Use Nx

Nx is a strong choice when you need:

Large monorepos with many apps and libraries
Clear dependency boundaries enforced by tooling
Advanced code generation for consistent project creation
Strong architectural enforcement through lint rules
Deep framework integration (Angular, React, Node)
Distributed task execution across multiple machines
Long-term scalability for enterprise organizations

Nx excels when structure becomes an advantage rather than overhead.

When to Use Turborepo

Turborepo works best when you want:

Quick monorepo adoption with minimal changes
Simple configuration without new concepts
Fast builds through caching as the primary benefit
Flexibility over strict rules for team autonomy
Minimal tooling overhead and learning curve
Existing npm/pnpm/yarn workflows to remain unchanged
Vercel deployment integration out of the box

For many JavaScript teams, this balance is ideal.

Common Mistakes to Avoid

When working with monorepos, teams often encounter these pitfalls that reduce the benefits.

1. Adopting a Monorepo Too Early

// ❌ Bad: Single small project in a monorepo
my-monorepo/
└── apps/
    └── my-only-app/  // Only one app, no shared code

// ✅ Good: Wait until you have shared concerns
// - Multiple apps sharing components
// - Shared utilities across projects
// - Team coordination challenges

2. Ignoring Dependency Boundaries

// ❌ Bad: Circular dependencies and spaghetti imports
import { something } from "../../../apps/other-app/src/utils";

// ✅ Good: Clear library boundaries
import { something } from "@myorg/shared-utils";

3. Not Configuring Caching Properly

// ❌ Bad: Missing outputs means cache can't restore
{
  "pipeline": {
    "build": {
      "dependsOn": ["^build"]
      // Missing outputs!
    }
  }
}

// ✅ Good: Explicit outputs for proper caching
{
  "pipeline": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**", ".next/**"]
    }
  }
}

4. Skipping CI Optimization

# ❌ Bad: Running everything on every PR
pnpm turbo run build test lint

# ✅ Good: Only run affected tasks
pnpm turbo run build test lint --filter=...[origin/main]

5. Inconsistent Package Versions

// ❌ Bad: Different React versions across packages
// apps/web/package.json
{ "dependencies": { "react": "^17.0.0" } }

// apps/dashboard/package.json
{ "dependencies": { "react": "^18.2.0" } }

// ✅ Good: Single version in root, workspace protocol
// package.json (root)
{ "dependencies": { "react": "^18.2.0" } }

// apps/web/package.json
{ "dependencies": { "react": "workspace:*" } }

6. Overcomplicating Library Structure

// ❌ Bad: Too many tiny libraries
libs/
├── button/
├── card/
├── modal/
├── input/
└── ... (100 more component libs)

// ✅ Good: Logical groupings
libs/
├── ui-components/         # All UI primitives
├── feature-auth/          # Auth feature
├── data-access-api/       # API client
└── util-formatting/       # Formatting utilities

Conclusion

Monorepos can greatly improve collaboration, consistency, and developer velocity when used correctly. Tools like Nx and Turborepo make monorepos practical by solving build performance and tooling challenges through intelligent caching, task orchestration, and dependency analysis. Nx provides a comprehensive platform with deep tooling, code generation, and architectural enforcement ideal for large organizations. Turborepo offers a lightweight, flexible approach that integrates seamlessly with existing workflows ideal for teams that value simplicity.

The right choice depends more on team needs, project scale, and organizational culture than raw features. Both tools significantly improve developer experience compared to naive monorepo setups or multi-repo architectures.

If you want to improve large-scale JavaScript workflows, read Modern ECMAScript Features You Might Have Missed. For automation and delivery strategies, see CI/CD for Node.js Projects Using GitHub Actions. To understand the Deno runtime that takes a different approach to package management, check out Introduction to Deno: A Modern Runtime for TypeScript and JavaScript. You can also explore the Nx documentation and the Turborepo documentation. By choosing the right monorepo tool, teams can scale codebases without slowing development.

1 Comment

Leave a Comment