WebPiki
tutorial

Monorepo Setup with Turborepo — A Practical Guide

Monorepo pros and cons, Turborepo core features, Nx comparison, and real-world project setup walkthrough.

Monorepo modules connected to a central hub

As teams grow and projects multiply — frontend, backend, shared libraries, design system — a question inevitably comes up. Do you manage each in its own repository, or put everything in one? The latter is a monorepo.

What's a Monorepo

One Git repository containing multiple projects (packages). Google, Meta, and Microsoft famously use them internally, but the pattern is useful at much smaller scales too.

A typical structure:

my-monorepo/
├── apps/
│   ├── web/          # Next.js frontend
│   ├── api/          # Express backend
│   └── admin/        # Admin dashboard
├── packages/
│   ├── ui/           # Shared UI components
│   ├── config/       # ESLint, TypeScript configs
│   └── utils/        # Shared utility functions
├── package.json
└── turbo.json

apps/ holds deployable applications. packages/ holds shared libraries. The key is that it all lives in a single repo.

Monorepo vs Polyrepo

A polyrepo (one repo per project) is the traditional approach. Both have clear trade-offs.

Monorepo Advantages

Easy code sharing. Put common utilities or type definitions in packages/utils and every app can import them directly. In a polyrepo setup, you'd publish an npm package, install it in each repo, manage versions... every change to shared code means going through that cycle.

Atomic changes. Modify a UI library's API and update the consuming app in one commit, one PR. Polyrepo requires: library PR, publish, app PR, deploy — multiple steps across multiple repos.

Consistent configuration. ESLint, TypeScript, Prettier configs in one place. Every project follows the same coding standards automatically.

Simpler dependency management. Share a single node_modules across projects (hoisting). No more "app A uses React 18.2 but app B is on 18.1" situations.

Monorepo Drawbacks

Repository size grows. Cloning and checkout can slow down. Git sparse checkout helps, but it's another thing to manage.

CI/CD complexity. You need to figure out which apps to rebuild when a package changes. Automating this is exactly what tools like Turborepo are for.

Access control is harder. If your org manages permissions at the repo level, a monorepo makes fine-grained access tricky.

Team conflicts. Multiple teams touching the same repo means more merge conflicts on main.

Turborepo — Making Monorepos Viable

Turborepo is a monorepo build system maintained by Vercel. It tackles the main pain points: slow builds and complex task orchestration. Written in Rust, so it's fast.

Caching

The biggest selling point. Turborepo hashes each task's inputs (source code, config files, env variables) and reuses previous results when nothing changed.

# first run — full build
npx turbo build
# cache miss → builds all packages

# run again without changes
npx turbo build
# cache hit → finishes instantly, skips all builds

Beyond local caching, there's remote caching. Build results from CI can be shared across the whole team. One person builds, everyone else skips that build for the same code. Vercel accounts get remote caching out of the box, or you can self-host a cache server.

Parallel Execution

Tasks without dependencies run simultaneously. If web, api, and admin builds are independent, they all run at once.

   ┌─── web (build) ──┐
   │                   │
   ├─── api (build) ──┤──→ done
   │                   │
   └── admin (build) ──┘

Parallelism scales with CPU cores. Tasks with dependencies execute in the correct order.

Task Pipelines

Define task dependencies in turbo.json:

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

The ^ in "dependsOn": ["^build"] means "run the build task in this package's dependencies first." If web depends on ui, then ui builds before web does.

outputs tells Turborepo what to cache. Set dist/** and next time the same inputs produce a build, Turborepo restores that folder instead of rebuilding.

Turborepo vs Nx

The two major monorepo tools.

TurborepoNx
MaintainerVercelNx
LanguageRustTypeScript + Rust (partially)
Config complexityLowMedium–High
ScopeBuild system focusedBuild + code generation + dependency graph visualization + plugins
CachingLocal + remoteLocal + Nx Cloud
Framework supportFramework-agnosticOfficial plugins for React, Angular, Node, etc.
MigrationDrops into existing projects easilyMay require restructuring

Turborepo keeps things simple and focuses on making builds fast. Add a turbo.json to an existing project and you're running. Vercel integration is seamless.

Nx offers way more features: code generators, dependency graph visualization, affected commands for impact analysis. The trade-off is a steeper learning curve and some project structure requirements.

For small to mid-size projects that want a quick start, Turborepo. For large teams needing comprehensive monorepo management, Nx might be the better fit.

Setting Up a Turborepo Project

Starting from scratch:

npx create-turbo@latest my-monorepo
cd my-monorepo

Adding to an existing project:

npm install turbo --save-dev

package.json Configuration

Define workspaces in the root package.json:

{
  "name": "my-monorepo",
  "private": true,
  "workspaces": ["apps/*", "packages/*"],
  "devDependencies": {
    "turbo": "^2.0.0"
  },
  "scripts": {
    "build": "turbo build",
    "dev": "turbo dev",
    "lint": "turbo lint",
    "test": "turbo test"
  }
}

"private": true prevents the root package from being published to npm. Always set this for monorepo roots.

Cross-Package Dependencies

To use packages/ui from apps/web:

// apps/web/package.json
{
  "name": "web",
  "dependencies": {
    "ui": "workspace:*"
  }
}

workspace:* tells npm/pnpm/yarn to resolve this dependency from the local monorepo package, not from the registry.

CI/CD Integration

GitHub Actions example:

name: CI
on: [push, pull_request]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci
      - run: npx turbo build lint test

Turborepo's caching means unchanged packages get skipped in CI, cutting build times significantly. Remote caching amplifies this further.

On Vercel, Turborepo is detected automatically — only changed apps get redeployed. That's one of the biggest wins of the Vercel + Turborepo combo.

When Not to Use a Monorepo

Monorepos aren't universal. Sometimes a polyrepo is genuinely better.

Unrelated projects. If there's no shared code and no coordinated deployment, separate repos make more sense.

Very large teams (hundreds of engineers). Standard Git tools struggle with massive monorepos. Google and Meta built custom version control systems for this. That's not something most teams can replicate.

Strict access control. If you need hard boundaries between project permissions, repo-level separation is more straightforward than GitHub CODEOWNERS.

Completely different tech stacks. A Go backend and a React frontend with different build systems, package managers, and dependency patterns don't gain much from sharing a repo.

Getting Started

If you're curious about monorepos, start small. Extract one shared utility or type definition package from an existing project.

  1. Create packages/shared/
  2. Move common types or utility functions into it
  3. Add turbo.json
  4. Verify the build works

That's enough to grasp the basics and see how Turborepo operates. If it helps, expand incrementally. Trying to design the perfect monorepo structure upfront is a recipe for never starting.

#monorepo#Turborepo#Nx#web development#DevOps

Related Posts