#Next.js#TypeScript

Monorepo Tooling: Turborepo vs. Nx

When packages and projects are scattered across multiple repositories, sharing code becomes difficult. This article explains monorepo benefits and costs, pnpm workspace basics, the tradeoffs between Turborepo and Nx, and which teams should adopt a monorepo.

Key Points

  • Topic Focus: Next.js、TypeScript
  • Suitable for: indie hackers, full-stack engineers, AI application developers
  • Core value: quickly understand background, practical paths, common pitfalls, and engineering advice
Author @admin·Published July 26, 2026·Updated July 26, 2026

Monorepo Tooling: Turborepo vs. Nx

If you have worked on projects where several repositories depend on one another, you have probably encountered this situation: the web repository needs to reuse form logic from the admin repository, so you either copy and paste it, creating a future synchronization problem, or publish it as an npm package and then go through the lengthy version bump → publish → 另一边升版本 → 重新装 process for every change. Do that three times in one day, and the whole day is gone.

A monorepo, meaning multiple projects in one repository, is designed to solve this problem. But it is not a silver bullet: repository growth, slower builds, and coarser permissions are unavoidable costs. This article begins with the pain of multiple repositories, covers the basics of pnpm workspace, compares today's two leading tools, Turborepo and Nx, and concludes with a practical directory structure and configuration example.


1. The Pain of Multiple Repositories: Why Adopt a Monorepo

Splitting related projects across multiple repositories, or a polyrepo, initially provides "independence," but the costs gradually emerge during collaboration:

  • Code is difficult to share: Reusing common logic such as components, hooks, utility functions, and type definitions across repositories requires either copying and pasting it, which allows the copies to drift, or publishing an npm package, which adds a very heavy process.
  • Dependency versions diverge: The web repository uses React 18, admin is still on 17, and shared-utils has installed two versions. Over time, upgrades become major cross-repository projects that nobody wants to touch.
  • Cross-repository changes are cumbersome: Renaming one API field requires simultaneous changes to the backend, web, admin, and mobile repositories, four PRs, four reviews, and merges in the correct order. Atomicity? There is none.
  • CI/CD becomes fragmented: Every repository has its own pipeline, configuration, and release scripts, so maintenance costs grow linearly with the number of repositories.

The core value of a monorepo can be summarized in one sentence: it lets "one logical change" be completed in one commit. Shared code is referenced directly as local packages, the workspace enforces version consistency, and the toolchain is configured only once.


2. The Costs of a Monorepo: Do Not Look Only at the Benefits

Combining projects is not free. Typical costs include:

  • A large repository: Cloning is slower, and new team members face more directories. Sparse checkout and shallow clone can help, but the cognitive load remains.
  • Slower builds: If the tooling is not sufficiently intelligent, changing one package triggers a full build. This is the central problem Turborepo and Nx address: build only the affected packages.
  • Coarse permission granularity: Git's permission model operates at the repository level and cannot grant access to individual subdirectories. Permissions become difficult when some projects are public while others contain sensitive internal code. Large companies typically mitigate this with a custom layer of CODEOWNERS and CI policies.
  • A unified toolchain is mandatory: Every project must use the same lint, test, and build tooling. This creates migration costs for teams whose projects already work well independently.

The conclusion is straightforward: a monorepo suits teams with intensive collaboration and frequent code sharing. Forcing several nearly unrelated projects into one repository will instead hold them back.


3. The Foundation: pnpm workspace

Whether you ultimately choose Turborepo or Nx, the underlying package management will almost always involve pnpm workspace. npm and Yarn also support workspaces, but pnpm is noticeably better in performance, disk usage, and protection against phantom dependencies.

Create a pnpm-workspace.yaml in the repository root:

yaml
# pnpm-workspace.yaml
packages:
  - "apps/*"
  - "packages/*"

Then declare the package name in packages/ui/package.json:

json
{
  "name": "@codenodex/ui",
  "version": "0.0.0",
  "private": true,
  "main": "./src/index.ts"
}

Reference it from apps/web using the workspace: protocol instead of hard-coding a version number:

json
{
  "dependencies": {
    "@codenodex/ui": "workspace:*",
    "next": "^15.0.0"
  }
}

workspace:* means always linking to the latest version in the current repository without going through the npm registry. Change a component in packages/ui, and apps/web sees it immediately: local packages are local files, so there is no "publish and upgrade" cycle. pnpm also uses hard links to share dependencies from its global store with every package, consuming an order of magnitude less disk space than npm or Yarn Classic.

At this point, you already have a usable monorepo. One layer is still missing: how to run tasks in bulk, including lint, test, and build, and how to cache results to avoid redundant builds. This is where Turborepo and Nx compete.


4. Turborepo: Lightweight Task Orchestration and Caching

Turborepo has a deliberately narrow scope. It does only two things: task orchestration and build caching. It does not take over the package manager's job or impose generators and a plugin system. Its configuration is a single turbo.json:

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

Several key concepts:

  • ^build: Run build for all of the package's dependencies before building the package itself. The topological order is calculated automatically.
  • outputs: Declares the paths of build artifacts. Turborepo uses them to determine cache hits. With identical inputs, including source code, the dependency lockfile, and environment variables, it skips the build and restores the artifacts directly.
  • Local and remote caching: A local cache already eliminates a large amount of repeated work. Connect Vercel's remote cache, or host your own, to share cached results between CI and team members, allowing even the first build on a new machine to complete almost instantly.

Typical usage:

bash
# 只构建受影响的包(本地开发常用)
turbo run build --filter=...[origin/main]

# 同时跑所有包的 lint / test / build,并行 + 缓存
turbo run lint test build

Turborepo's advantages are clear:

  • Simple configuration: It takes five minutes to learn and adds almost no overhead to JS/TS projects.
  • Parallel tasks with topological ordering: Builds can be several times faster than running scripts directly.
  • Mature caching: Remote caching works out of the box within the Vercel ecosystem.
  • Low intrusiveness: It does not force you to change package managers or directory conventions.

Its limitations are equally clear: there is no code generator, dependency graph visualization, or framework-specific "one-click" plugin, such as scaffolding a Next.js application automatically. These are precisely Nx's strengths.


5. Nx: A Comprehensive Platform with Generators and a Plugin Ecosystem

Nx takes a different approach: a complete engineering platform. In addition to task orchestration and caching, where its capabilities are comparable to Turborepo, Nx provides:

  • Generators: Generate convention-compliant components, libraries, and applications with one command, including built-in best practices.
  • Dependency graphs: nx graph visualizes dependencies between packages, making the scope of a refactor immediately visible.
  • A plugin ecosystem: Official plugins for Angular, React, Next.js, NestJS, Node, and more provide complete sets of generators, migration scripts, and builders.
  • The affected command: Precisely identifies which packages a commit affects and runs test, build, and lint only for those packages.
  • Distributed task execution: Distributes tasks across multiple machines for parallel execution, making it suitable for extremely large repositories.

A typical nx.json looks like this:

json
{
  "tasksRunnerOptions": {
    "default": {
      "runner": "nx/tasks-runners/default",
      "options": {
        "cacheableOperations": ["build", "lint", "test"],
        "remoteCache": { "enabled": true }
      }
    }
  },
  "namedInputs": {
    "default": ["{projectRoot}/**/*", "sharedGlobals"]
  },
  "targetDefaults": {
    "build": {
      "dependsOn": ["^build"],
      "inputs": ["default", "^default"],
      "outputs": ["{projectRoot}/dist"]
    }
  }
}

Here is a generator example that creates a new React library:

bash
nx g @nx/react:lib buttons --directory=packages/buttons

This command automatically creates the directory, generates package.json, configures the build tools, adds the appropriate tsconfig paths, and even makes the library immediately visible in nx graph.

Nx's advantages:

  • A lifesaver for large projects: For teams managing dozens or hundreds of packages, mixing multiple frameworks, and requiring enforced constraints, Nx's engineering capabilities are irreplaceable.
  • Ready-to-use plugins: Angular and NestJS projects are especially smooth.
  • Extensibility: It extends from task orchestration through CI orchestration and release automation.

Its costs:

  • A steep learning curve: nx.json, project.json, generators, executors, and plugins introduce many concepts, slowing down new team members.
  • High intrusiveness: It tends to take over the directory structure, build configuration, and migration process. Once a project depends deeply on it, migrating away is painful.
  • Relatively complex configuration: Using Nx for a small project is overkill.

6. Turborepo vs. Nx: How to Choose

This is not simply a question of which tool is "better." They have different positions. Here is a practical comparison:

DimensionTurborepoNx
Onboarding costVery low; one turbo.json is enoughHigh; many concepts and configurations
Core capabilitiesTask orchestration + cachingTask orchestration + caching + generators + dependency graphs + plugins
Suitable scaleSmall to medium, primarily JS/TSLarge, multi-framework, polyglot
EcosystemVercel ecosystem; works well with Next.jsRich plugin ecosystem; strong for Angular and NestJS
IntrusivenessLow; almost transparentHigh; takes over project structure
Remote cachingWorks out of the box with Vercel; can be self-hostedCan use self-hosted caching

A blunt rule of thumb:

  • Your repository is primarily JS/TS, medium-sized, and the team prefers to manage its own toolchain → Turborepo.
  • You manage dozens of packages, mix multiple frameworks, and need generators and strict constraints → Nx.
  • You already use Angular or NestJS → Nx is almost the default choice.

For medium-sized projects, I personally lean toward Turborepo. It does fewer things, but it does them well enough, and its low intrusiveness means it can be replaced at any time. Nx is more like a long-term investment: you accept its learning curve and lock-in in exchange for its capabilities.


7. Which Teams Should Adopt a Monorepo

First answer "should we adopt one?" and only then ask "which tool should we use?"

Scenarios where you should adopt one:

  • Multiple applications or packages frequently share code, such as component libraries, utility functions, type definitions, and business hooks.
  • You need a unified toolchain for lint, test, CI, and releases instead of maintaining a separate setup in every repository.
  • Cross-repository changes are frequent and require atomicity, such as changing one interface across several projects simultaneously.
  • The team cares about dependency version consistency, such as requiring the same major React version everywhere.

Scenarios where you should not adopt one:

  • There is only one project and little code to share.
  • The projects use completely different technology stacks, the teams operate independently, and they rarely collaborate.
  • The team is small, and the cost of learning monorepo tooling exceeds the benefits.

A monorepo is a product of collaboration density, not an engineering ritual that merely "looks more advanced." If collaboration in the current polyrepo is not painful, do not force a monorepo in the name of "engineering maturity."


8. Practical Example: One App and Multiple Packages

The following is a Turborepo and pnpm workspace structure I commonly use. It suits most small and medium-sized JS/TS teams:

text
my-monorepo/
├── apps/
│   ├── web/              # Next.js 应用
│   └── admin/            # 后台管理
├── packages/
│   ├── ui/               # 共享组件库
│   ├── utils/            # 工具函数
│   ├── tsconfig/         # 共享 tsconfig 预设
│   └── eslint-config/    # 共享 eslint 配置
├── package.json
├── pnpm-workspace.yaml
├── turbo.json
└── tsconfig.base.json

Root package.json:

json
{
  "name": "my-monorepo",
  "private": true,
  "scripts": {
    "build": "turbo run build",
    "dev": "turbo run dev",
    "lint": "turbo run lint",
    "test": "turbo run test"
  },
  "devDependencies": {
    "turbo": "^2.0.0",
    "typescript": "^5.5.0"
  },
  "packageManager": "[email protected]"
}

apps/web/package.json:

json
{
  "name": "@codenodex/web",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build"
  },
  "dependencies": {
    "@codenodex/ui": "workspace:*",
    "@codenodex/utils": "workspace:*",
    "next": "^15.0.0",
    "react": "^19.0.0"
  }
}

For daily development, run pnpm dev inside a package or pnpm run dev at the repository root to let Turborepo start every app in parallel. For builds, turbo run build --filter=...[origin/main] builds only affected packages. Change a Button in packages/ui, and every app that depends on it automatically receives the latest code during the next build, without publishing or bumping a version.

The benefit of this structure is that it can evolve incrementally. Turborepo may be sufficient today. If the repository eventually outgrows it and you want to migrate to Nx, the directory conventions do not require major changes. This is also why I recommend that small and medium-sized teams start with Turborepo: it preserves an exit path.


Summary

  • A monorepo addresses the pain of cross-repository collaboration, including code sharing, version consistency, atomic changes, and a unified toolchain. Its costs are a larger repository, slower builds, and coarser permissions.
  • pnpm workspace is the foundation of nearly every modern monorepo, and the workspace: protocol makes local package references essentially free.
  • Turborepo focuses on task orchestration and caching. It is lightweight, simple to configure, and minimally intrusive, making it suitable for medium-sized teams working primarily with JS/TS.
  • Nx offers a comprehensive feature set, including generators, dependency graphs, a plugin ecosystem, and affected. It suits large, multi-framework teams that require strong engineering constraints, at the cost of a steep learning curve and greater intrusiveness.
  • Choose based on collaboration density and scale: not every team should adopt a monorepo, and not every monorepo should use Nx.

In one sentence: A monorepo is not the final destination of engineering maturity, but the natural choice once collaboration becomes dense enough. Always ask "does this hurt today?" before asking "which tool should we use?"

Related

Related Content