, updated · 3 min read
Building a monorepo with Turborepo and Vite
When a monorepo is worth it, how Turborepo and pnpm work together, how to keep the cache trustworthy, and what it costs.

The Problem A Monorepo Solves
A monorepo is worth the setup when several things need to change together. A component used by three apps, a set of types shared between a web client and a mobile one, a utility that three teams keep reimplementing slightly differently. Put those in one repository and a change becomes a single commit that either works everywhere or fails everywhere.
If you have one application, you do not need one. The tooling below solves coordination problems, and coordination problems only exist once there is something to coordinate.
What Turborepo Actually Does
Turborepo is a task runner that understands the dependency graph between your packages. Two things follow from that.
It caches task output keyed on the inputs. If nothing in a package changed, its build does not run again, locally or in CI. On a repository with several apps this is the difference between a two minute pipeline and a twelve minute one.
It runs independent tasks in parallel while respecting the order where order matters. Your shared library builds before the apps that import it, but the four apps that do not depend on each other build at the same time.
Getting The Cache Right
The cache is the whole point, and the part people get wrong. If a task's declared inputs do not match what it actually reads, you will get a cache hit that produces stale output, which is far more confusing than a slow build.
Be explicit about outputs so the cache knows what to store. Be careful with environment variables, since a task that reads one but does not declare it will happily reuse output built with a different value. When something looks wrong, clear the cache first and treat a stale hit as the prime suspect.
Package Boundaries Are The Real Design
The tooling is straightforward. Deciding what belongs in which package is the part that takes judgement.
- Keep shared packages honest about their environment. A utility imported by a React Native app must not touch
windowordocument. That constraint is easy to state and easy to violate accidentally. - Separate tokens from components. Design values with no framework attached can be consumed by anything, including a native app that cannot import your web components.
- Resist the shared utilities bin. A package named
utilsbecomes a dumping ground that everything depends on and nobody understands.
Where pnpm Fits
pnpm's workspaces link packages to each other, and its content-addressed store keeps one copy of a shared dependency rather than one per app, which noticeably cuts install time and disk use.
Its strictness is the more valuable part. A package can only import what it actually declares, so the accidental dependency that works locally and breaks in CI is caught at the point it is introduced.
The Costs Worth Knowing
Monorepos are not free. Continuous integration gets more complicated, because you want to build only what changed rather than everything on every push. Onboarding is heavier, since a new developer clones considerably more than one application. Tooling occasionally assumes a single package at the repository root and needs coaxing.
None of these are reasons to avoid one. They are reasons to adopt one when the coordination pain is real rather than anticipated.
A Reasonable Starting Point
Start with your applications in one directory and shared code in another. Add a package when the same code has been copied a second time, not in advance of it. Keep the shared layer small, because every package there is one more thing to version, test and reason about. The monorepo that works is usually the one that grew into its structure.



