The pendulum in software architecture swings between two extremes: everything in one place, and everything split apart. Monorepos bring multiple projects into a single repository — and done right, they’re genuinely powerful. Done wrong, they’re a slow, tangled mess.

Let’s look at what monorepos actually offer, where they fall short, and which tooling makes them work at scale.

What Is a Monorepo?

A monorepo is a single version-controlled repository containing multiple projects. Those projects can be independent applications, shared libraries, or both.

my-company/
  apps/
    web/          ← React frontend
    mobile/       ← React Native
    api/          ← Node.js API
  packages/
    ui/           ← shared component library
    utils/        ← shared utilities
    config/       ← shared ESLint, tsconfig, etc.
  package.json    ← root workspace config

This is NOT a monolith. The apps are still independently deployable. The code just lives in one repo.

Why Teams Choose Monorepos

Atomic cross-project changes

In a polyrepo (one repo per project), updating a shared utility means:

  1. PR in utils repo → merge → publish new version
  2. PR in web to upgrade the version
  3. PR in api to upgrade the version
  4. Coordinate releases across 3 repos

In a monorepo, one PR changes the utility and all consumers at once. No version coordination. No stale dependency hell.

Shared tooling and standards

One .eslintrc, one tsconfig.base.json, one CI pipeline definition. Consistency is automatic.

Easier refactoring

When you rename a type that’s used across 5 packages, your IDE and TypeScript compiler see all of them. You refactor once, not five times.

Unified dependency management

One node_modules at the root (or near-root). No diverging versions of the same library across packages. Easier to audit for security issues.

The Tradeoffs

CI/CD complexity

If every PR triggers a full build and test of all projects, CI gets slow fast. Monorepos require incremental builds — only rebuild what changed.

Repository size

Over years, a monorepo can grow very large. Git operations slow down. You need shallow clones and sparse checkout for developer machines.

Access control

Some teams need different access controls per project (contractors who can see the frontend but not the API). Monorepos make this harder.

Tooling investment

Monorepos require dedicated tooling. Without it, they’re chaos. With it, they’re powerful.

Turborepo

Turborepo (by Vercel) is the simplest way to get monorepo task caching and parallel execution:

// turbo.json
{
  "$schema": "https://turbo.build/schema.json",
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": [".next/**", "dist/**"]
    },
    "test": {
      "dependsOn": ["^build"],
      "outputs": []
    },
    "lint": {
      "outputs": []
    }
  }
}
# Install
npm install turbo --save-dev

# Build only affected packages (with cache)
npx turbo run build

# Test in parallel
npx turbo run test --parallel

Turborepo caches task outputs locally and remotely. If you haven’t changed packages/utils, its build output is restored from cache — not rebuilt.

Best for: frontend-focused monorepos, teams already using Vercel, projects with 2–20 packages.

Nx

Nx (by Nrwl) is more opinionated and more powerful. It has a full plugin ecosystem, generators, and a project graph that understands dependencies deeply.

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

# Add apps and libraries
nx g @nx/react:app web
nx g @nx/node:app api
nx g @nx/js:lib utils

# Run only affected projects
nx affected --target=build
nx affected --target=test

Nx’s affected command is its killer feature: it analyzes the dependency graph and only runs tasks for projects affected by your changes.

Changed: packages/utils
Affected: apps/web (imports utils), apps/api (imports utils)
Not affected: apps/mobile (doesn't import utils)
→ Only build and test web and api

Project graph visualization:

nx graph  # Opens browser with interactive dependency graph

Best for: larger teams, backend+frontend monorepos, projects needing generators and scaffolding, Angular-heavy projects (Nx’s origin).

Turborepo vs Nx

TurborepoNx
Learning curveLowMedium-high
Caching✅ Local + remote✅ Local + remote
Affected detectionBasicAdvanced (graph-based)
Generators
Plugin ecosystemSmallLarge
Framework opinionsNoneStrong
Best forSimple setupsComplex setups

Workspace Configuration

Both tools use npm/yarn/pnpm workspaces underneath:

// package.json (root)
{
  "name": "my-monorepo",
  "private": true,
  "workspaces": ["apps/*", "packages/*"],
  "scripts": {
    "build": "turbo run build",
    "test": "turbo run test",
    "lint": "turbo run lint"
  }
}
// packages/utils/package.json
{
  "name": "@myorg/utils",
  "version": "0.0.1",
  "main": "./dist/index.js",
  "scripts": {
    "build": "tsc",
    "test": "vitest"
  }
}
// apps/web/package.json
{
  "dependencies": {
    "@myorg/utils": "*"  // Reference local package by name
  }
}

When to Use a Monorepo

Good fit:

  • Multiple apps sharing significant code (design system, utilities, types)
  • A platform team managing multiple services with shared contracts
  • Teams doing frequent cross-project refactors

Poor fit:

  • Independent projects with no shared code
  • Teams with strict access control requirements per project
  • Very small teams where the tooling overhead isn’t worth it

Key Takeaways

  • Monorepos enable atomic cross-project changes, shared tooling, and easier refactoring
  • They require investment in CI tooling — incremental builds are non-negotiable
  • Turborepo is simpler and lower-overhead; Nx is more powerful and more opinionated
  • Both provide caching and affected detection that make large monorepos fast
  • A monorepo is not a monolith — projects remain independently deployable