Skip to main content
  1. Posts/

One Repo to Rule Them All

·1171 words·6 mins
Photograph By Heather Wilde
Blog JavaScript Web Development
Table of Contents

The Shared Types Problem
#

You have a frontend and a backend. They share types — User, Order, ApiResponse. So you copy-paste the type definitions into both projects. A week later, you add a field to User in the backend. You forget to update the frontend. The frontend breaks in production because it doesn’t know about the new field.

The fix seems obvious: put shared code in one place. But “one place” means one repository — a monorepo. And a monorepo means you need a way to build, lint, test, and deploy multiple packages without losing your mind. That’s where Turborepo comes in.

What Turborepo Actually Does
#

Turborepo is a build orchestrator. It doesn’t replace your tools — it runs them smarter. You still use tsc to compile, Vite to bundle, and Vitest to test. Turborepo figures out:

  1. What depends on what — if @repo/client depends on @repo/types, build types first
  2. What can run in parallel — if @repo/auth and @repo/todos are independent, build both simultaneously
  3. What hasn’t changed — if @repo/types hasn’t changed since the last build, skip it entirely

That third point is the game-changer. Turborepo hashes your source files, config, and environment variables. If the hash matches a previous run, it restores the cached output instead of rebuilding. First build takes 6 seconds. Second build with no changes? 100 milliseconds.

The Config That Makes It Work
#

Everything lives in turbo.json. Here’s what mine looks like in the BHVR project :

{
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "inputs": [
        "src/**/*.ts",
        "src/**/*.tsx",
        "tsconfig.json",
        "vite.config.ts"
      ],
      "outputs": ["dist/**"],
      "env": ["NODE_ENV", "VITE_*"]
    },
    "dev": {
      "cache": false,
      "persistent": true
    },
    "test": {
      "dependsOn": ["^build"],
      "inputs": ["src/**/*.ts", "**/*.test.ts", "vitest.config.*"],
      "outputs": ["coverage/**"],
      "env": ["NODE_ENV", "CI"]
    }
  }
}

The magic is in the fields:

dependsOn: ["^build"] — that ^ means “build my dependencies first.” If @repo/client depends on @repo/types, Turborepo builds types before client. Without the ^, it would only run tasks within the same package.

inputs — the files Turborepo hashes for cache invalidation. If none of these files changed, the cache is valid. Be specific: include src/**/*.ts but not node_modules/. The more precise your inputs, the fewer unnecessary rebuilds.

outputs — what the task produces. These get cached and restored on cache hits. dist/** for builds, coverage/** for tests. Without this, Turborepo has nothing to cache.

cache: false — for dev servers. You always want to run the dev server fresh, never restore it from cache.

persistent: true — marks long-running tasks (dev servers, watchers). Turborepo won’t wait for these to “complete” before running other tasks.

Minimal vs Detailed Config
#

I looked at the r8y project by Ben Davis — another Turborepo + Bun monorepo — and their config is much leaner:

{
  "tasks": {
    "dev": { "cache": false, "persistent": true },
    "check": { "dependsOn": ["^check"] },
    "lint": { "dependsOn": ["^lint"] }
  }
}

No inputs. No outputs. No env. This works — Turborepo has sensible defaults. But without explicit inputs, it hashes more files than necessary. Without outputs, nothing gets cached. For small projects, the defaults are fine. For larger ones, explicit config pays for itself in cache hit rates.

Config StyleCache AccuracySetup EffortBest For
Minimal (no inputs/outputs)Lower (over-invalidates)2 minutesSmall projects, getting started
Detailed (explicit everything)High (precise invalidation)30 minutesProduction monorepos, CI/CD

The Monorepo Structure
#

My BHVR project is organized like this:

my-bhvr/
├── backend/
│   ├── auth/         # @repo/auth — authentication service
│   ├── gateway/      # @repo/gateway — API gateway (Hono)
│   └── todos/        # @repo/todos — todo service
├── frontend/
│   └── client/       # @repo/client — React + Vite
├── packages/
│   ├── types/        # @repo/types — shared TypeScript types
│   ├── ui/           # @repo/ui — shared UI components
│   └── eslint-config/ # @repo/eslint-config — shared lint rules
├── turbo.json
└── package.json

The packages/ directory holds shared code. The frontend imports @repo/types and @repo/ui. The gateway imports @repo/types. Changes to @repo/types trigger rebuilds only in packages that depend on it — not everything.

// frontend/client/package.json
{
  "dependencies": {
    "@repo/types": "workspace:*",
    "@repo/ui": "workspace:*"
  }
}

workspace:* resolves to the local package. No npm publishing needed. Change a type in packages/types/, and the frontend sees it immediately.

Global Dependencies: The Cache Nuke
#

Some files affect everything. When tsconfig.json at the root changes, every package’s build could be different. Turborepo has globalDependencies for this:

{
  "globalDependencies": [
    "**/.env",
    "turbo.json",
    "package.json",
    "bun.lock",
    "tsconfig.json"
  ]
}

When any of these files change, ALL task caches are invalidated. Use this sparingly — too many global dependencies means your cache gets nuked on every minor change.

One Command, All Services
#

turbo dev starts every dev server in the monorepo simultaneously. Four services, one command, one terminal. Turborepo runs them in parallel (they’re all persistent: true, cache: false), shows all output in a TUI, and handles the dependency ordering if any dev task depends on a build step.

Compare this to the alternative: four terminal tabs, four cd commands, four separate start commands, and manually remembering which services need to start in which order.

Filtering: Run What You Need
#

When you’re only working on the frontend:

turbo dev --filter=@repo/client...    # client + its dependencies
turbo build --filter=packages/*       # only shared packages
turbo test --filter=@repo/gateway     # only gateway tests

The ... suffix means “this package and everything it depends on.” So @repo/client... starts the client dev server and builds @repo/types and @repo/ui first.

Why Not Nx?
#

Nx is the other major monorepo tool. It’s more powerful — code generators, dependency visualization, file-level change detection (vs Turborepo’s task-level), and a plugin ecosystem for React, Angular, Node, and more.

But it’s also more complex. Turborepo is a turbo.json file. Nx is nx.json, project.json per package, workspace generators, and its own CLI with dozens of commands.

TurborepoNx
Config files1 (turbo.json)Many (nx.json + per-package)
Learning curveLowHigh
Code generationNone (use your tools)Built-in scaffolding
Change detectionTask-level hashFile-level graph
Plugin ecosystemMinimalExtensive
Best forJS/TS, simplicityLarge/polyglot, enterprise

I chose Turborepo because I wanted task orchestration and caching, not a framework. It does one thing well and stays out of the way. For a BHVR stack project with Bun + Hono + Vite + React, that’s exactly what I needed.

The CI/CD Payoff
#

Where Turborepo really shines is CI. The first pipeline run builds everything and populates the cache. Every subsequent run only rebuilds packages that actually changed. With remote caching enabled, this cache is shared across all pipeline runs and team members.

PR #1: Full build — 45 seconds
PR #2: Changed @repo/client only — 12 seconds (types and backend cached)
PR #3: Changed @repo/types — 30 seconds (everything that depends on types rebuilds)
PR #4: Changed README only — 3 seconds (nothing to rebuild)

Combined with Bun’s install speed (3 seconds vs npm’s 30), a CI pipeline that used to take 5 minutes finishes in under a minute. That’s not a micro-optimization — it’s the difference between “I’ll wait for CI” and “CI already passed.”

Aaron Yong
Author
Aaron Yong
Building things for the web. Writing about development, Linux, cloud, and everything in between.

Related

Buntime Funtime
·1331 words·7 mins
Photograph By Cesar Carlevarino Aragon
Blog JavaScript Web Development
Bun as a runtime, package manager, and the all-in-one promise — from benchmarks to a real project
The Contract
·785 words·4 mins
Photograph By Romain Dancre
Blog Software Engineering Web Development
REST API design patterns that save your future self from debugging nightmares
Building With Hugo
·1204 words·6 mins
Photograph By Nick Morrison
Blog Hugo Web Development
How I built this website with Hugo and the Blowfish theme