Duplicated code, monorepos, and the three tools: npm workspaces, Turborepo and Nx
When to use each. Over-engineering is expensive — starting with the simplest option is almost always right.
There's a moment in every project where you realize you have the same code in two places. A component that renders posts in your landing, and the same component — with slight variations — in your blog. A getPosts() function querying Supabase, duplicated across two apps. When you change something, you have to remember to change it in both. And when you forget, silent drift.
It happened to me this week while building Agentikas. I had two sites from two Next.js repos:
agentikas.ai/blog— landing with integrated blog*.blog.agentikas.ai— multi-tenant platform for other blogs
Same posts (same Supabase), two frontends, two implementations. Every time I added a feature — a like button, an "author picks" section, a tag filter — I had to build it twice. The last time I caught it, the like button was on one side and not the other. The reader probably thought it hated them.
That's when it's time to decide: monorepo.
What problem a monorepo solves
A monorepo is simply a repository containing several related projects. Instead of this:
agentikas-ai/ (repo 1)
src/components/PostCard.tsx
src/data/blog.ts
agentikas-blog/ (repo 2)
src/components/PostCard.tsx ← copy
src/data/blog.ts ← copy
You have this:
agentikas/ (single repo)
apps/
landing/
blog/
dashboard/
packages/
blog-ui/ ← one PostCard
blog-data/ ← one getPosts()
Each app imports from the shared package. You change something once. It propagates to every app. End of duplication.
But not all monorepos are equal. Three dominant tools, each with its own philosophy. Let's walk through them in order of complexity.
npm workspaces: the minimum viable
npm workspaces is a native npm feature since v7. Nothing to install. Just add this to your root package.json:
{
"workspaces": ["apps/*", "packages/*"]
}
npm now knows your packages are local. When you run npm install at the root:
- Installs every dependency of every package
- Creates a single
node_modulesat the root (hoisting) - Links local packages via symlinks
If apps/blog has "@agentikas/blog-ui": "*" in its dependencies, npm creates a symlink at apps/blog/node_modules/@agentikas/blog-ui → packages/blog-ui. No publishing, no versions. Your package lives on disk and imports like any npm module.
What you get:
- Zero new tooling
- Zero configuration
- Available in any modern Node project
What it doesn't give you:
- No build cache
- No automatic parallel task execution
- If a shared package breaks, the consumer's
next buildwill catch it, but there's no visualization of the dependency graph
For small-to-medium projects — 3 to 5 apps, a handful of shared packages — npm workspaces is enough. More than enough. It's what I'm using in Agentikas.
Turborepo: when the build starts to hurt
Turborepo is a Vercel tool that sits on top of npm workspaces (or pnpm, or yarn). Its value prop is one word: cache.
Turborepo analyzes your monorepo's dependency graph. It knows apps/blog depends on packages/blog-ui. When you run turbo build:
- Topologically orders tasks (base packages first, consumers after)
- Parallelizes whatever can be parallelized
- Caches each task's output based on a hash of source + dependencies
The second time you run turbo build, if nothing in packages/blog-ui changed, Turborepo doesn't recompile. It returns the cached output in milliseconds. In CI with remote cache (Vercel or your own backend), a push that changes only one app can skip complete rebuilds of the others.
What you get:
- Incremental builds (only recompile what changed)
- Parallelization without configuration
- Remote cache shared between devs and CI
- One command for everything:
turbo dev,turbo test,turbo build
What it costs:
- A
turbo.jsonwith task pipelines - Learning the
tasksanddependsOnsyntax - If you're on Vercel, remote cache is free. If not, spin up your own or pay
Turborepo shines when you have 5+ apps, builds beginning to exceed a minute, and CI becoming a bottleneck. Before that, it's a tool that adds ceremony without solving a real pain.
Nx: the option for larger teams
Nx is the heaviest of the three. It came out of the Angular world but today supports every framework. It does the same thing as Turborepo — cache, dependency graph, parallel execution — with more ambition.
Nx isn't just a build tool. It's a code generation system. You can define generators for your conventions: "create a new app with this template, this linter, these tests, this folder structure." You can auto-migrate between framework versions with codified migrations. You can visualize the dependency graph in a browser with nx graph.
What you get:
- Everything Turborepo has, plus
- Code generators to standardize new apps/packages
- Automatic migrations between versions
- A cloud platform with analytics, remote cache, optimized CI
What it costs:
- Considerable configuration
- Learning curve
- A more opinionated philosophy
- If you don't leverage generators and migrations, you're paying complexity without extracting value
Nx makes sense when you have 10+ apps and want to enforce consistent conventions, multiple teams in parallel needing governance, and willingness to invest setup time for long-term productivity. For a solo developer or small team, it's overkill.
The decision: which to pick when
No universal answer. The practical rule I follow:
Your situationTool Just hit duplication and want to solve it fastnpm workspaces 3–4 apps, reasonable builds (< 1 min)npm workspaces 5+ apps and CI starts to hurtTurborepo Large team, many apps, governance neededNxThe temptation to over-engineer is real. Easy to read a Vercel post about Turborepo and think "I need this." But if next build takes 800ms on your machine, Turborepo saves you 800ms. And the first turbo build has no cache anyway. You're adding a dependency, a config file, and a new mental model to optimize something that isn't your bottleneck.
The problems you'll hit (that nobody tells you)
All three tools advertise "just works." Not true. These are the actual problems you'll run into:
Symlinks and modern bundlers. Turbopack — Next.js's new bundler — restricts following symlinks outside the project directory. When your app imports a local workspace package, the package lives outside apps/your-app/. Turbopack may refuse to compile it unless you configure turbopack.root correctly. Found out the hard way.
Hoisting and duplicate dependencies. npm workspaces hoists common dependencies to the root. Sometimes this breaks tools that expect each app to own its node_modules/next. Happens more with Turbopack than classic webpack.
Transpiling TypeScript packages. If your shared package is TypeScript without a prebuild step, Next.js needs to know via transpilePackages: ["@agentikas/blog-ui"]. Forget it and you get a cryptic error.
Cloudflare Pages / Vercel cache with monorepos. Both platforms support monorepos, but the build command and output directory change. A deploy that used to work can break because the platform is looking in the wrong place.
Tests sharing state. If your tests cached modules assuming each app had its own copy, a monorepo with shared packages can break those tests in subtle ways.
Conclusion: start small
My honest advice after using all three:
- Start with npm workspaces. Solves 80% of duplication pain.
- Only if builds become slow (and only then), add Turborepo on top. It's additive, not disruptive.
- Don't jump to Nx unless you have a team that needs it.
The worst decision is over-sizing infrastructure before you have the problem. The second worst is letting duplication grow. The right one is the middle ground: the simplest tool that solves your current pain.
For Agentikas, I'm going with npm workspaces. Three apps, a couple of shared packages. The day this grows enough to migrate to Turborepo, it'll be a few hours of work. And I'll have spent months not worrying about pipelines, caches, and generators I didn't need.
At Agentikas we build infrastructure for the agentic web. Our blog is open source and you can see exactly how the monorepo is structured in github.com/agentikas/agentikas-blog.
Comments
Loading comments…
Sign in on your dashboard to join the conversation.