Turborepo: Monorepos for Multi-App Shared Libraries

A monorepo is not “a very large Git repository” as a flex—it is a coordination technology. When two or more applications genuinely share UI, data clients, types, or build tooling, shipping them from separate repos without automation turns every shared change into a multi-step release theater: bump a package, wait for publish, upgrade consumers, chase version skew, repeat. Monorepos trade some repository size and tooling complexity for atomic commits, a single CI graph, and the ability to refactor across boundaries the way you refactor inside one app.

Turborepo sits in a crowded space next to Nx, Bazel-style tooling, and ad hoc npm workspaces. For many React and Next.js teams in 2026, Turborepo wins on simplicity of configuration, first-class integration with Vercel’s remote cache, and a task model that maps cleanly onto package.json scripts. It does not try to be a full enterprise build system; it tries to run the right scripts in the right order, as fast as possible, with caching that actually works.

When a monorepo is the right move

Reach for a monorepo when at least one of these is true:

  • Two or more deployable apps share a design system, hooks, or typed API clients.
  • Multiple teams work on related surfaces (marketing site, logged-in dashboard, mobile shell) and need consistent TypeScript settings.
  • You want atomic changes across packages—rename a prop in @repo/ui and fix every consumer in one commit.

Skip it when you have one app and no realistic second consumer. Publishing a private npm package from a single repo is often less machinery than bootstrapping workspaces, turbo.json, and shared config packages for hypothetical reuse.

Scaffolding in minutes

The official starter wires sensible defaults for pnpm workspaces and Next.js applications:

npx create-turbo@latest

Choose pnpm and the app templates your organization prefers (often Next.js on the web side plus an optional Expo app). The generator gives you a working pipeline so you can focus on package boundaries instead of fighting path aliases for a week.

Repository shape

A typical Turborepo layout looks like:

monorepo/
├── apps/
│   ├── web/
│   ├── dashboard/
│   └── mobile/
├── packages/
│   ├── ui/
│   ├── database/
│   ├── utils/
│   └── typescript-config/
├── turbo.json
└── pnpm-workspace.yaml

apps/* are deployable units. packages/* are libraries consumed by those apps. Keeping that distinction explicit prevents “accidental apps” that import server-only code into client bundles. Shared TypeScript and ESLint configs live in packages so extends stays one line and compiler upgrades happen once.

Shared UI package: exports and peer dependencies

The most common footgun in shared React packages is double React—two copies on the page, broken hooks, and impossible context. Mark React as a peer dependency so the application’s React instance is authoritative:

{
  "name": "@repo/ui",
  "exports": {
    "./button": {
      "import": "./src/button.tsx",
      "types": "./src/button.tsx"
    }
  },
  "peerDependencies": {
    "react": "^19.0.0",
    "react-dom": "^19.0.0"
  },
  "devDependencies": {
    "@repo/typescript-config": "workspace:*"
  }
}

Subpath exports keep public API small and tree-shaking honest. Consumers write import { Button } from '@repo/ui/button' instead of reaching into deep paths that you cannot rename later.

Shared TypeScript baseline

Centralize strictness. A packages/typescript-config/base.json might look like:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "jsx": "react-jsx"
  }
}

Apps and libraries extend this file with their own include paths and any framework-specific options. The win is not the exact flags—it is that every package agrees on what “strict” means, so a type error in packages/utils is not silently looser than the dashboard.

Task graph: dependsOn encodes reality

turbo.json describes how tasks compose. The ^ prefix means “depend on the same task in dependencies first,” which is how builds respect package order:

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

outputs tell Turborepo what to cache; omit them and you still get orchestration, but you leave performance on the table. Mark dev as persistent so Turbo knows the process stays alive. For lint and type-check, depending on ^build is a pragmatic choice when generated artifacts (GraphQL code gen, protobuf stubs) must exist first—if your repo has no codegen, you might drop that dependency to keep feedback loops faster.

Remote caching and CI

Local caching already deduplicates work across branches. Remote caching shares those artifacts across machines—CI runners, teammates, preview deploy pipelines. After linking:

npx turbo login
npx turbo link

subsequent runs can skip work that another environment already proved correct. Marketing numbers like “80%+ faster CI” are situational; what is reliable is the model: hashes of inputs determine cache keys, and unchanged dependency graphs stop rebuilding the world. The operational caveat is secret hygiene: ensure caches cannot leak private environment into artifacts you would not publish.

Turborepo versus Nx

Turborepo tends to fit teams that want minimal config, strong Next.js ergonomics, and Vercel-aligned caching. Nx brings a deeper plugin ecosystem, generators, and graph visualization that larger polyglot orgs exploit heavily. Neither is “wrong.” A useful 2026 heuristic: start with Turborepo when your world is mostly TypeScript, React, and Next.js; evaluate Nx when you need first-class orchestration across many languages or heavy codegen-driven workflows.

Opinionated pitfalls to avoid

Do not create twenty micro-packages on day one. Let seams emerge from real reuse. Do not share “everything utils” without boundaries—packages/utils becomes the new src/services dumping ground. Prefer a few well-named packages (ui, api-client, config) over a forest of one-function packages that explode install and mental overhead.

Monorepos fix coordination; they do not fix ownership. If two teams need independent release cadences and conflicting roadmaps, a monorepo can still work—with discipline—but it is not a substitute for product alignment. When organizational independence is the real constraint, runtime integration (the next section’s topic) enters the picture.

Used with intent, Turborepo makes shared React libraries feel like internal open source: explicit exports, typed contracts, and a task graph that mirrors your dependency graph instead of fighting it.