Setting Up a Monorepo With Turborepo: Lessons From Our Migration
Six months ago, our codebase was a single Next.js application with everything crammed into one package.json. The tool components, the marketing pages, the API routes, the shared utilities — all in one repo with one build command. It worked fine when we had 20 tools and two contributors. It became painful at 200 tools and five contributors.
This is the story of our migration to Turborepo, including the parts that did not go according to plan.
Why We Migrated
Three problems pushed us over the edge:
First, build times. A single change to a utility function triggered a full rebuild of the entire application. With 200+ tool components, TypeScript checking alone took 45 seconds. Adding the Next.js build on top meant 3-4 minutes for every PR.
Second, dependency conflicts. Our tool components needed specific versions of libraries (like monaco-editor for the code editors) that conflicted with what the marketing pages needed. We were stuck resolving peer dependency warnings constantly.
Third, code ownership. Five contributors working in one flat structure meant constant merge conflicts in package.json, unclear ownership of shared utilities, and no way to run CI only on the packages that changed.
The Target Architecture
We settled on this structure:
The goal was clear boundaries. The web app imports from packages. Packages never import from the web app. Each package has its own tsconfig, its own build command, and its own tests.
The Migration Process
We did the migration incrementally over three weeks. Here is the order that worked:
Week 1: Set up Turborepo scaffolding. Create the workspace root package.json with the workspaces field. Create packages/config with shared TypeScript and ESLint configs. Everything still builds and deploys normally — we just moved config files.
Week 2: Extract packages/ui. We identified 40+ components that were pure UI (no business logic, no data fetching). We moved them into packages/ui with their own package.json and tsconfig. The web app imported them via the workspace protocol. This was the riskiest step because every import path changed.
Week 3: Extract packages/tools. The tool components were the biggest chunk. We moved them into their own package with their own build step using tsup. The web app dynamically imports them for code splitting.
What Went Wrong
The biggest headache was Tailwind CSS. When components live in a separate package, Tailwind's content scanning does not pick them up automatically. We had to add the package paths to the web app's tailwind.config.ts content array. This seems obvious in retrospect but cost us a full day of debugging "why are my styles missing in production."
Second problem: TypeScript project references. We initially tried using TypeScript's composite mode with project references for faster type checking. It worked locally but broke in CI because the build order was not deterministic. We reverted to simple workspace dependencies with explicit build steps in the Turborepo pipeline.
Third: hot module replacement. In development, changes to packages/ui did not trigger HMR in the web app. We had to configure the Next.js transpilePackages option and set up Turborepo's watch mode. Getting this right took two days of trial and error.
The Turborepo Pipeline Configuration
Our turbo.json pipeline defines the dependency graph:
The build task depends on the build task of all workspace dependencies (topological ordering). The type-check task depends on the type-check of dependencies. The test task has no dependencies and runs in parallel across all packages.
The key insight: mark tasks as "persistent" if they are long-running (like the dev server). Use the dependsOn field to express that web's build needs tools and ui to be built first.
Cache Performance
After the migration, Turborepo's caching transformed our CI times. A change to the web app's marketing pages only rebuilds apps/web — packages are served from cache. A change to packages/ui rebuilds ui and web (because web depends on ui) but skips tools. Cache hit rates in our CI are around 70%, which translates to average CI times dropping from 4 minutes to 90 seconds.
Local development also benefits. Running turbo build after changing a single file takes 3 seconds instead of 45 because unchanged packages are cached.
Remote Caching
We enabled Turborepo's remote caching with Vercel. This means CI cache is shared across all contributors. When developer A builds packages/ui, developer B gets a cache hit on their next push. Remote caching alone cut our monthly CI minutes by 40%.
Would I Do It Again?
Absolutely, but I would do it earlier. The migration was harder than it needed to be because we waited until the codebase was already large. If you are starting a project that you know will grow, set up the monorepo structure from day one. The overhead is minimal and the benefits compound over time.
The tooling around monorepos in 2026 is mature. Turborepo handles the hard parts (caching, orchestration, incremental builds). pnpm workspaces handle dependency hoisting. The combination is reliable and well-documented.
Start with two packages: apps/web and packages/shared. Add more packages only when you have a concrete reason to split. Over-splitting is as bad as not splitting at all.
Written by AIDToolStack Editorial
The AIDToolStack editorial team reviews developer tools, AI products, and MCP servers with hands-on testing and honest assessments. We use the tools we review every day in production.
References & Further Reading
Related Posts
Sponsor Our Newsletter
Reach thousands of developers who are actively evaluating AI tools, MCP servers, and dev infrastructure. Our weekly newsletter goes to engaged technical decision-makers.
All sponsored content is clearly labeled per our editorial policy.