Skip to content

Monorepo vs Polyrepo in 2026: What I Actually Reach For Now

Monorepo vs Polyrepo in 2026: What I Actually Reach For Now

I migrated a five-app codebase from a monorepo to two polyrepos last winter. Then I moved one of those polyrepos back into a monorepo six months later. So I have opinions, and most of them contradict each other.

Short version for the impatient: I default to a pnpm workspaces monorepo with Turborepo for anything where two or more apps share an internal package that genuinely changes. If they share nothing real, separate repos. The interesting cases sit in between, and that’s where I’ve made every mistake worth making.

Here’s what I keep coming back to after a year of swinging between the two.

The thing nobody tells you about monorepos

Everyone talks about how a monorepo makes refactoring easier because you change a type in one package and the call sites in five apps catch on. That part is true and I love it. What nobody warned me about was how the local dev story degrades the moment your install graph gets even slightly busy.

I had a Next.js app, a Hono API, an Expo mobile app, and a shared db package wrapping Drizzle. Fresh clone took 90 seconds, which felt fine. pnpm dev would happily start, but the Expo bundler and the Next dev server would fight over Watchman file watches on my Mac and one of them would crash within ten minutes. I spent two weeks blaming pnpm, then Turborepo, then Watchman. It was a peer-dep conflict in react, of course it was.

The point is: monorepos move the failure mode from “where is this dep?” to “everyone shares one node_modules graph and one of them is angry”. You will deal with hoisted dependencies. You will read more pnpm-lock.yaml than you ever wanted to. That tradeoff is fine if you’re getting real value from shared code. It’s a tax with no benefit if you aren’t.

Where polyrepos still earn their keep

When I moved the API and the mobile app out into separate repos, I expected to miss the cross-app refactoring. I didn’t, much. The API and the mobile app shared exactly one thing: a generated OpenAPI client. Publishing that as a small package to a private npm registry, with a pnpm version patch && pnpm publish script, gave me roughly 90% of what the monorepo gave me, minus the headaches.

Things that get easier in a polyrepo:

  1. Onboarding. A new contributor clones the API repo. They run two commands. They are running tests. They don’t have to think about packages they don’t care about.
  2. CI runtimes. Each repo has its own pipeline that runs in 90 seconds instead of one combined pipeline that runs in eight minutes and re-tests things you didn’t touch.
  3. Cleanup. When you sunset a service, you archive the repo. You don’t go on a six-week tour of every monorepo workspace deleting references to it.

The honest answer is that most teams of two or three people building a couple of small services should not have a monorepo. They should have separate repos and a private package or two. The monorepo machinery only earns its keep when there’s enough shared code that the tooling cost is cheaper than the duplication cost.

Turborepo vs Nx: what I actually pick now

I’ve shipped projects on both. They are not interchangeable, even though articles online keep treating them that way.

Turborepo is the right call when your monorepo is mostly JavaScript and TypeScript, mostly apps plus shared packages, and you don’t want to learn a new mental model. The config is roughly the same shape as a package.json script section, plus caching. I can teach a new dev how Turborepo works in 15 minutes. Here’s the entire turbo.json for one of my client projects:

{
  "$schema": "https://turbo.build/schema.json",
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": [".next/**", "dist/**", "!.next/cache/**"]
    },
    "lint": { "dependsOn": ["^build"] },
    "test": { "dependsOn": ["^build"], "outputs": ["coverage/**"] },
    "dev": { "cache": false, "persistent": true }
  }
}

That’s it. The ^build syntax means “build my workspace deps first”. The cache hits make CI go from eight minutes back down to two on a small change. The Turborepo docs explain the rest in roughly the time it takes to drink a coffee.

Nx is the right call when you actually need plugins. The Nx generators, the dependency graph visualizer, the project boundary enforcement, and the affected-projects detection are genuinely impressive when you have 30 apps and 80 libraries. I ran an Nx workspace at a previous gig with around 25 React apps and it was great. The Nx mental model docs are worth reading even if you end up picking Turborepo, because the way they think about project graphs will sharpen how you think about your own.

For 2 to 6 apps, I pick Turborepo every time. Above 10 apps, I’d seriously consider Nx. The middle ground is the messy part, and most blog posts pretend it isn’t.

The Next.js monorepo trap I keep walking into

If you are setting up a Next.js monorepo, the thing that will bite you is transpilePackages. Two years ago, you had to use next-transpile-modules. Now it’s built in. Half the tutorials online still use the old way.

Old way (don’t do this):

const withTM = require('next-transpile-modules')(['@acme/ui', '@acme/db']);

module.exports = withTM({
  reactStrictMode: true,
});

New way:

// next.config.js
module.exports = {
  reactStrictMode: true,
  transpilePackages: ['@acme/ui', '@acme/db'],
};

You also want to make sure your workspace packages don’t ship compiled output if they don’t need to. If @acme/ui’s package.json points "main" and "types" at the source .ts files (and not a dist/), Next will compile them inline and you skip the build step entirely during dev. That alone shaved 30 seconds off my cold start.

I wrote about a related Next.js footgun in the app router vs pages router post so I won’t repeat all of it here.

How I’d set one up today

If you’re starting fresh, the path I’d take:

mkdir my-monorepo && cd my-monorepo
pnpm init
cat > pnpm-workspace.yaml <<EOF
packages:
  - "apps/*"
  - "packages/*"
EOF
pnpm add -D -w turbo typescript
mkdir -p apps packages

The -w flag installs at the workspace root, which is where Turborepo lives. Each app or package gets its own package.json with internal deps referenced like:

{
  "name": "web",
  "dependencies": {
    "@acme/db": "workspace:*",
    "@acme/ui": "workspace:*"
  }
}

That workspace:* is doing the heavy lifting. pnpm resolves it to a symlink. You change the package, every consumer sees it on next save. No npm link, no yalc, no rebuilding to verify a change. The pnpm workspaces docs cover the few edge cases that bite, mostly around peer deps and publishing.

I would not reach for Lerna in 2026. Lerna had a rough decade and the parts that mattered are now in pnpm. I would not reach for Yarn workspaces either. They work, but the ecosystem has moved on and your future hires will know pnpm.

When I’d just use polyrepos and move on

Concrete checklist I run through before starting a monorepo:

  • Do at least two of your apps actually need to import the same code, today? Not “they might someday”?
  • Are the teams owning these apps going to coordinate on releases?
  • Do you have a working CI that can cache builds, or are you about to throw a Turborepo cache in and call it done?

If two of those three are no, ship a polyrepo. Publish a tiny package or two to GitHub Packages or a private npm registry. Get back to writing the actual app.

The hardest lesson from the last year is that I picked monorepos because they felt sophisticated, not because the codebase needed one. The codebase that actually benefited was the one where I had a shared types package, a shared UI package, and three apps that all consumed both. That kind of structure earns the tooling cost. A “we might share something eventually” structure does not.

What to do this week

Pick one repo where you’ve been arguing internally about which side of the line to be on. Open the apps. Count, honestly, how many lines of code they share via internal packages. If the answer is under 500, you don’t need a monorepo and you’re paying the tax for nothing. If the answer is over 5,000 and growing, the polyrepo is silently costing you in copy-pasted utilities. The middle band is where you go look at my actual project setups in the work portfolio and stop reading blog posts.

If you want a related read, my note on Bun vs Node in production hits similar “what I run versus what looks good in a thread” territory.