Nx vs. Turborepo for TypeScript Monorepos: Comparing Build Caching
AI generated
<T>
type
TypeScript · Monorepo · Build Tooling
Nx vs. Turborepo for TypeScript Monorepos
Comparing build caching and task pipelines

A TypeScript monorepo without build orchestration rebuilds every package on every commit, even when only one line changed in a single package. Nx and Turborepo solve this problem with task graphs, caching and affected detection, but with different philosophies and different feature scope.

18 min read Nx · Turborepo · Task Pipelines · Caching TypeScript 5.x · Node.js 20+

1. Why build orchestration becomes a problem in a TypeScript monorepo

Once a TypeScript monorepo grows beyond a handful of packages, a plain npm run build per package is no longer enough. Without orchestration, every CI script builds all packages in the wrong order, ignores dependencies between them, or repeats work that has not changed at all since the last run. With ten packages this might still be tolerable, but with fifty packages every pipeline becomes a test of patience and every pull request waits minutes for a build that is ninety percent redundant.

This is exactly where Nx and Turborepo step in. Both analyze the dependency structure of a TypeScript monorepo as a directed graph, detect which packages are affected by a change, and cache the result of completed tasks. The difference is not in the underlying idea but in feature scope: Nx ships a complete ecosystem of generators, plugins and a dedicated editor extension, while Turborepo deliberately stays lean and focuses on task execution and caching. Which tool fits better depends heavily on team size and the desired level of control over the build process.

2. Nx in overview: project graph and generators

Nx was originally built by a former Angular team and positions itself as a full development platform for a TypeScript monorepo, not just a build runner. At its core is the project graph: Nx scans every package.json and project.json, reads imports between packages, and builds a complete dependency map from them. The command nx graph visualizes this network interactively in the browser, which helps considerably when onboarding new team members into a grown monorepo.

Nx also ships generators that create new packages, libraries or components from predefined templates. Running nx generate @nx/js:library shared-utils creates a new package in seconds with a correctly configured tsconfig.json, test setup and lint rules, consistent with every other package in the TypeScript monorepo. This significantly reduces copy paste errors when creating new packages, but it also introduces a learning curve because Nx enforces its own conventions for project structure and configuration.


{
  "targetDefaults": {
    "build": {
      "dependsOn": ["^build"],
      "inputs": ["production", "^production"],
      "outputs": ["{projectRoot}/dist"],
      "cache": true
    },
    "test": {
      "inputs": ["default", "^production"],
      "cache": true
    },
    "lint": {
      "inputs": ["default", "{workspaceRoot}/.eslintrc.json"],
      "cache": true
    }
  },
  "namedInputs": {
    "default": ["{projectRoot}/**/*", "sharedGlobals"],
    "production": ["default", "!{projectRoot}/**/*.spec.ts"]
  }
}

3. Turborepo in overview: pipelines and minimalism

Turborepo follows a deliberately narrower approach for a TypeScript monorepo. Instead of shipping its own generators, plugins or editor integration, the tool focuses on exactly one task: running tasks from the individual workspace package.json scripts in the correct order while caching as much as possible. The configuration lives in a single file, turbo.json, which defines per task what it depends on and which outputs should be cached.

This minimalism is both the greatest strength and the greatest limitation of Turborepo. Teams already working with npm, pnpm or Yarn workspaces who simply want faster, cached builds are often productive with Turborepo in under an hour, because no existing project structure needs to be rebuilt. Teams looking for a complete tooling ecosystem with code generation and built in migration scripts for a growing TypeScript monorepo, however, will not find that in Turborepo alone and must combine additional tools.


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

4. Configuring task pipelines: dependsOn and outputs

The central mechanism in both tools is the declarative description of task dependencies. The key dependsOn with the ^ prefix means the same thing in both systems: run this task first in every package this package depends on. A build task with dependsOn: ["^build"] ensures that a shared library is always built before the package that imports it, without a developer having to maintain the order manually in a shell script.

The outputs field is equally decisive because it tells the cache which files must be stored after a successful task. If a directory like dist/** is missing from the configuration, caching formally still works, but a cache hit will not restore any actual build artifacts. A common beginner mistake in a TypeScript monorepo is setting the outputs path incorrectly, for example because the tsconfig.json value for outDir does not match the path configured in the pipeline.

5. Caching in detail: local, remote and invalidation

Local caching works similarly in both tools: a hash is computed from a task's input files, relevant environment variables and the task definition itself. If none of these inputs change, Nx or Turborepo returns the cached result directly from the local file system without re running the task. This considerably speeds up repeated local builds in a TypeScript monorepo, especially when switching between branches where many packages remained unchanged.

Remote caching is where the two tools diverge financially and technically. Nx Cloud offers a hosted remote cache with distributed task execution across multiple CI machines. Turborepo supports remote caching through Vercel's infrastructure or self hosted alternatives such as a custom S3 compatible implementation. In both cases: a cache hit in the CI pipeline of a TypeScript monorepo saves not just time but actual compute cost when build minutes are billed by usage.


# Nx: check local and remote cache status
npx nx build shared-utils --skip-nx-cache=false

# Connect to Nx Cloud (enable remote cache)
npx nx connect-to-nx-cloud

# Turborepo: connect remote cache with Vercel
npx turbo login
npx turbo link

# Deliberately bypass cache, e.g. after a dependency update
npx turbo run build --force

6. Affected commands: building only impacted packages

The biggest practical win for a growing TypeScript monorepo lies in affected commands. Instead of building and testing every package on every pull request, both tools compute from the Git diff which packages changed directly or transitively. Nx uses nx affected --target=test and compares against the main branch by default. Turborepo achieves the same with turbo run test --filter=...[main], a filter syntax that looks less intuitive at first glance but performs the same Git based computation.

In a TypeScript monorepo with fifty packages, where a typical pull request touches only two or three, CI runtime often drops to a tenth of what a full build would take. It matters to configure the comparison branch correctly, because an incorrectly set base commit either causes too many packages to be marked as affected, or worse, causes actually affected packages to be missed, letting a bug slip into production unnoticed.

7. Migrating from plain workspaces to Nx or Turborepo

An existing TypeScript monorepo that so far only relies on npm or pnpm workspaces can be migrated incrementally with either tool. Turborepo only needs a turbo.json at the root and works immediately with existing package.json scripts, with no need to change package structures. The migration effort often takes just a few hours, because Turborepo respects the existing workspace configuration instead of replacing it.

Nx offers a similarly gentle entry with npx nx init, which automatically detects existing scripts and converts them into Nx targets without immediately forcing the full Nx project structure with project.json files. For teams that later want to use generators and the project graph too, gradually moving to the full Nx configuration pays off, because additional analysis features such as automatic detection of circular dependencies in the TypeScript monorepo become available.

8. Integration into CI/CD pipelines

The investment in Nx or Turborepo pays off most visibly in the CI pipeline. The usual setup consists of a single checkout followed by an affected command for lint, test and build, instead of three separate jobs that go through every package individually. For the cache to be reused across CI runs, either a remote cache must be configured or the local cache folder must be explicitly persisted between pipeline stages, for example via GitLab CI caches or GitHub Actions artifacts.

A detail that is frequently overlooked in practice: for affected commands to work correctly in CI, the pipeline needs access to the full Git history, not just the last commit. A shallow checkout with --depth=1 prevents Nx or Turborepo from finding the comparison commit, causing the entire TypeScript monorepo to be incorrectly marked as affected. The fix is usually simple: set fetch-depth: 0 in GitHub Actions or GIT_DEPTH: 0 in GitLab CI.

9. Nx vs. Turborepo side by side

Both tools solve the core problem of a TypeScript monorepo, but differ significantly in scope, learning curve and operating model. The following overview summarizes the most important decision criteria.

Criterion Nx Turborepo Practical note
Setup effort Medium to high, own ecosystem Low, one config file Turborepo gets small teams started faster
Code generation Generators for packages, libs, components Not built in Relevant when new packages are created often
Project graph visualization nx graph in the browser Text output only Helpful in large, hard to overview repos
Remote caching Nx Cloud, paid beyond a certain volume Vercel or self hosted Check pricing model before scaling up
Distributed task execution Yes, via Nx Cloud Agents No, local caching plus remote cache only Relevant for very large test suites

For small to mid sized teams that mainly want faster, cached builds without a major overhaul, Turborepo is usually the more pragmatic entry point into a TypeScript monorepo. Larger organizations with many teams, frequently emerging packages and the need for distributed test execution benefit more from the full Nx ecosystem, accepting in return a steeper learning curve and more upfront configuration effort.

Mironsoft

TypeScript architecture, monorepo tooling and CI/CD optimization

TypeScript monorepo with slow CI runs?

We analyze existing workspace structures, introduce Nx or Turborepo matched to team size, and set up affected builds plus remote caching for noticeably shorter pipeline runtimes.

Tooling selection

Evaluate Nx or Turborepo matched to team size and growth plan

Migration

Move existing workspaces to task pipelines step by step

CI optimization

Affected builds and remote caching in GitLab CI or GitHub Actions

10. Summary

Nx and Turborepo solve the same underlying problem of a growing TypeScript monorepo: avoiding unnecessary, redundant builds through a task graph, caching and affected detection. Turborepo convinces through minimalism and a fast entry via a single turbo.json, while Nx offers a complete ecosystem with generators, project graph visualization and distributed task execution. The choice between the two is rarely wrong as long as it matches actual team size and growth speed.

Regardless of the chosen tool: affected commands and correctly configured outputs are the two levers with the biggest effect on the CI runtime of a TypeScript monorepo. Teams that set these two points up cleanly usually see a noticeable reduction in pipeline duration within the first week, long before more complex topics like remote caching or distributed test execution even become relevant.

Nx vs. Turborepo — the key takeaways

Task graph

Both tools build a dependency graph from package imports and run tasks in the correct order via dependsOn.

Caching

A hash of inputs, environment and task definition decides a cache hit. outputs must be set correctly.

Affected commands

Git diff based detection of affected packages often reduces CI time to a fraction of a full build.

Decision guide

Turborepo for a fast, lean entry. Nx for generators, project graph and distributed test execution.

11. FAQ: Nx vs. Turborepo for TypeScript Monorepos

1What is the fundamental difference?
Nx is a complete platform with generators and project graph. Turborepo focuses on task execution and caching via one config file.
2Combine both tools?
Technically possible but not recommended due to duplicated caches and configuration. Commit to one tool.
3outputs missing from the pipeline?
Task is cached, but artifacts are not restored on a cache hit. Subsequent tasks find empty directories.
4Whole monorepo marked as affected?
Usually a shallow Git checkout with --depth=1. Set fetch-depth: 0 or GIT_DEPTH: 0 in CI.
5Remote caching for small teams?
Often pays off later with few CI runs. Local caching already brings noticeable improvements.
6Migrating from npm workspaces?
Add turbo.json at root, define pipelines for existing scripts, then call turbo run build. Structure stays unchanged.
7Distributed test execution with Turborepo?
No, only local and remote caching. Nx with Nx Cloud Agents is the better fit for distributed task execution.
8Specific project structure for generators?
Most reliable with project.json per package. Without these files generators are more limited.
9How is the cache hash computed?
From inputs, environment variables and task definition. If nothing changes, the cached result is returned.
10Advantage of the Nx project graph?
Interactive, visual display in the browser eases onboarding into large TypeScript monorepos.