edit_document// BLOG_POST.md

Monorepos with Turborepo: Manage Multiple Packages Without the Pain

//

, ,

As projects grow, the “one repo per package” approach creates friction. Shared libraries need publishing and versioning. A change to a utility package means updating five downstream repos. Integration testing across packages requires coordinating multiple repositories, branches, and CI pipelines. Monorepos solve this by keeping all related packages in a single repository with shared tooling and atomic commits. Turborepo makes monorepos practical for JavaScript and TypeScript projects by adding intelligent caching, task parallelization, and incremental builds.

Why Turborepo

Without a build orchestrator, a monorepo is just a folder with multiple package.json files. Run npm run build in the root and you have no guarantee packages build in the right order, no caching of unchanged packages, and no parallelization. Turborepo solves all three: it reads your dependency graph, builds packages in topological order, runs independent tasks in parallel, and caches every task output. The second build of an unchanged package takes zero seconds because Turborepo replays the cached output.

Project Structure

my-monorepo/
  apps/
    web/              # Next.js frontend
      package.json    # name: "@myorg/web"
    api/              # Express backend
      package.json    # name: "@myorg/api"
  packages/
    ui/               # Shared React component library
      package.json    # name: "@myorg/ui"
    config-eslint/    # Shared ESLint config
      package.json    # name: "@myorg/eslint-config"
    config-ts/        # Shared tsconfig
      package.json    # name: "@myorg/tsconfig"
    utils/            # Shared utility functions
      package.json    # name: "@myorg/utils"
  package.json        # Root: workspaces, devDependencies
  turbo.json          # Turborepo pipeline config

Root Configuration

// package.json (root)
{
  "name": "my-monorepo",
  "private": true,
  "workspaces": ["apps/*", "packages/*"],
  "devDependencies": {
    "turbo": "^2.3.0",
    "typescript": "^5.7.0"
  },
  "scripts": {
    "build": "turbo run build",
    "dev": "turbo run dev",
    "lint": "turbo run lint",
    "test": "turbo run test",
    "clean": "turbo run clean"
  }
}
// turbo.json
{
  "$schema": "https://turbo.build/schema.json",
  "globalDependencies": ["**/.env.*local"],
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**", ".next/**", "!.next/cache/**"]
    },
    "dev": {
      "cache": false,
      "persistent": true
    },
    "lint": {
      "dependsOn": ["^build"]
    },
    "test": {
      "dependsOn": ["build"]
    },
    "clean": {
      "cache": false
    }
  }
}

The "dependsOn": ["^build"] syntax means “run the build task of all dependencies first.” The ^ prefix indicates upstream dependencies. If @myorg/web depends on @myorg/ui, Turborepo builds ui before web automatically. The outputs array tells Turborepo which files to cache.

Cross-Package Dependencies

// apps/web/package.json
{
  "name": "@myorg/web",
  "dependencies": {
    "@myorg/ui": "workspace:*",
    "@myorg/utils": "workspace:*",
    "next": "^15.0.0",
    "react": "^19.0.0"
  }
}

// packages/ui/package.json
{
  "name": "@myorg/ui",
  "main": "./dist/index.js",
  "types": "./dist/index.d.ts",
  "dependencies": {
    "@myorg/utils": "workspace:*",
    "react": "^19.0.0"
  },
  "scripts": {
    "build": "tsup src/index.ts --format esm,cjs --dts",
    "dev": "tsup src/index.ts --format esm,cjs --dts --watch",
    "lint": "eslint src/"
  }
}

The workspace:* protocol tells the package manager to resolve the dependency from the local workspace instead of the npm registry. Changes to @myorg/ui are immediately available in @myorg/web without publishing.

Caching in Action

# First build: everything runs
$ turbo run build
@myorg/utils:build   - cache miss, 1.2s
@myorg/ui:build      - cache miss, 2.8s
@myorg/api:build     - cache miss, 3.1s
@myorg/web:build     - cache miss, 8.4s
Total: 15.5s

# Second build: nothing changed
$ turbo run build
@myorg/utils:build   - cache hit, replaying output
@myorg/ui:build      - cache hit, replaying output
@myorg/api:build     - cache hit, replaying output
@myorg/web:build     - cache hit, replaying output
Total: 0.3s

# After changing a file in @myorg/utils:
$ turbo run build
@myorg/utils:build   - cache miss, 1.2s  (changed)
@myorg/ui:build      - cache miss, 2.8s  (depends on utils)
@myorg/api:build     - cache miss, 3.1s  (depends on utils)
@myorg/web:build     - cache miss, 8.4s  (depends on ui)
Total: 15.5s

Turborepo hashes source files, dependencies, and environment variables to determine cache keys. If the inputs have not changed, the outputs are replayed from cache. Enable Remote Caching (Vercel or self-hosted) to share the cache across CI runners and team members. A build that ran on your coworker’s machine can be replayed on yours in milliseconds.

CI/CD Integration

# .github/workflows/ci.yml
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: 22, cache: 'npm' }
      - run: npm ci
      - run: npx turbo run lint test build
        env:
          TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
          TURBO_TEAM: ${{ vars.TURBO_TEAM }}

With remote caching enabled, CI only rebuilds packages that actually changed since the last successful run. A monorepo with 10 packages where only one changed completes in seconds instead of minutes.

When Monorepos Are Not the Answer

Monorepos add complexity. They require tooling (Turborepo, Nx, or Bazel), enforce consistent dependency versions, and make shallow checkouts harder for very large repos. If your packages are genuinely independent (different teams, different release cadences, no shared code), separate repositories are simpler. But if you find yourself publishing internal packages just to share code between two or three apps, a monorepo will eliminate that overhead entirely.

Further reading: Turborepo Documentation | Structuring a Repository | Caching in Turborepo


arrow_circle_right// POST_NAVIGATION

forum// COMMENTS

Leave a Reply

Your email address will not be published. Required fields are marked *