Monorepo Basics — When and Why Teams Move to Monorepos
Day 20 of 40 — React System Design Series

Hi, I’m Richa — a Senior Frontend Engineer with 5+ years of experience building scalable, production-grade web interfaces for enterprise and consumer applications. I work primarily with React, TypeScript, and modern frontend architectures, focusing on component systems, performance, and maintainability. Most of my experience comes from building real-world products in regulated domains like banking and insurance, where clarity, reliability, and long-term ownership matter more than quick demos. Through this blog, I write about frontend engineering fundamentals, scalable UI design, problem-solving, and the lessons I’ve learned working on large codebases. My goal is to share practical insights — not shortcuts — for developers who want to grow strong engineering foundations. I also mentor early-career developers and strongly believe that curiosity, asking the right questions, and understanding why something works are more important than memorizing tools. If you’re serious about improving as an engineer, you’re in the right place.
Hey everyone, Richa here! 👋
Day 20. Last day of Week 4. 🎉
And today we zoom out from component-level architecture to something that affects the whole engineering org.
Monorepos.
This is one of those topics that sounds like infrastructure and not frontend — until you're at a company that moves to one, or moves away from one, and suddenly it affects every single thing you do.
I want to give you the real picture. Not "monorepos are great" or "monorepos are terrible." But: what problem they solve, when they're worth it, and what you're signing up for.
First — what is a monorepo?
A monorepo (monolithic repository) is a single git repository that contains multiple projects.
Not a monorepo:
github.com/mycompany/frontend-app ← separate repo
github.com/mycompany/backend-api ← separate repo
github.com/mycompany/design-system ← separate repo
github.com/mycompany/mobile-app ← separate repo
A monorepo:
github.com/mycompany/platform ← one repo
apps/
web/ ← React frontend
api/ ← Node.js backend
mobile/ ← React Native app
packages/
ui/ ← shared design system
utils/ ← shared utilities
types/ ← shared TypeScript types
config/ ← shared ESLint, TypeScript, Prettier config
One repo. Multiple apps. Shared packages between them.
The problem monorepos solve
To understand why teams move to monorepos, you need to understand what breaks in multi-repo setups as teams grow.
Problem 1 — Shared code is painful
Your React app and mobile app both need a formatCurrency function. In multi-repo, you:
Duplicate it (it drifts, bugs get fixed in one but not the other)
Or publish it as an npm package (now every change requires: update package → publish → bump version in every consumer → update → deploy)
A simple bug fix in a shared utility takes a day of version management. In a monorepo: edit the function, done. Every app gets the change immediately.
Problem 2 — Cross-repo changes are a nightmare
Your API changes a response shape. You need to update:
The backend (repo 1)
The frontend types (repo 2)
The mobile app types (repo 3)
Three pull requests. Three code reviews. Three merges. Coordinating them to deploy at the same time. Getting the timing wrong means a broken app in production.
In a monorepo: one PR. One review. One merge. Atomic change.
Problem 3 — Shared config drifts
ESLint rules, TypeScript config, Prettier settings — each repo has its own version. They drift over time. One team upgrades TypeScript, others don't. You get inconsistency across projects.
In a monorepo: one shared config/ package. Everyone uses the same rules.
The two main monorepo tools in the React ecosystem
Turborepo — the modern default for JS/TS monorepos
npx create-turbo@latest
Turborepo handles:
Task pipeline — knows that
buildinwebdepends onbuildinuifirstCaching — if nothing changed, skip the build and use the cached output
Parallel execution — runs independent tasks simultaneously
Nx — more opinionated, more powerful, better for large enterprises
npx create-nx-workspace@latest
Nx adds:
Project graph (visual dependency map)
Affected commands — only rebuild what changed
Code generators for consistent scaffolding
Better for very large teams with complex dependency graphs
For a new React monorepo in 2025: start with Turborepo. It's simpler to set up and covers 90% of what most teams need. If you outgrow it — migrate to Nx.
What a React monorepo looks like with Turborepo
platform/
apps/
web/ ← Next.js or Vite React app
package.json
src/
docs/ ← Documentation site (Docusaurus, etc.)
package.json
src/
packages/
ui/ ← Shared component library
package.json → name: "@platform/ui"
src/
Button.tsx
Input.tsx
Modal.tsx
index.ts
utils/ ← Shared utility functions
package.json → name: "@platform/utils"
src/
formatDate.ts
formatCurrency.ts
cn.ts
index.ts
types/ ← Shared TypeScript types
package.json → name: "@platform/types"
src/
api.types.ts
user.types.ts
index.ts
config/
eslint/ ← Shared ESLint config
package.json → name: "@platform/eslint-config"
typescript/ ← Shared TypeScript config
package.json → name: "@platform/tsconfig"
turbo.json ← Turborepo pipeline config
package.json ← Root workspace config (pnpm/yarn workspaces)
pnpm-workspace.yaml
turbo.json — defines how tasks depend on each other:
{
"$schema": "https://turbo.build/schema.json",
"pipeline": {
"build": {
"dependsOn": ["^build"], // build deps first
"outputs": [".next/**", "dist/**"]
},
"dev": {
"cache": false,
"persistent": true
},
"lint": {
"outputs": []
},
"test": {
"outputs": ["coverage/**"]
}
}
}
Now from the root:
# Build everything in the right order (ui first, then apps that depend on it)
pnpm turbo build
# Dev — start all apps and watch all packages simultaneously
pnpm turbo dev
# Lint everything
pnpm turbo lint
# Only run affected tasks (what changed since last git commit)
pnpm turbo build --filter="[HEAD^1]"
Using a shared package in your app
Once the ui package exists:
// apps/web/package.json
{
"dependencies": {
"@platform/ui": "*", // ← workspace reference, not npm
"@platform/utils": "*"
}
}
// apps/web/src/pages/LoginPage.tsx
import { Button, Input } from '@platform/ui';
import { validateEmail } from '@platform/utils';
export function LoginPage() {
return (
<form>
<Input type="email" placeholder="Email" />
<Input type="password" placeholder="Password" />
<Button type="submit">Sign in</Button>
</form>
);
}
The Button and Input come from the shared ui package. Changes to the design system are reflected in every app immediately. No version bumps. No npm publish.
The real costs — what you're signing up for
Monorepos have real costs. Be honest with yourself about whether your team is ready for them.
Cost 1 — Tooling complexity
Setting up Turborepo or Nx correctly takes time. CI pipelines need updating. Your build system is now more complex. Someone needs to own and maintain it.
Cost 2 — Slow CI if not configured well
A naive CI setup runs all tests for all apps on every PR. With 10 apps, that's 10x slower. Fixing this requires proper affected-file detection and caching — which is non-trivial to set up.
Cost 3 — Package manager discipline
You need a package manager that supports workspaces well. pnpm (recommended) or yarn. npm workspaces work but are slower. Every developer needs to understand workspace dependencies.
Cost 4 — IDE performance
Opening a large monorepo in VS Code can be slow. TypeScript language server has to index many more files. Teams often open only a sub-folder of the monorepo rather than the root.
When to move to a monorepo
Good signals:
You have shared code (UI components, utils, types) that needs to stay in sync across multiple apps
Cross-repo changes (API + frontend + mobile) are taking too long to coordinate
Your shared packages are becoming hard to version and publish
You have 3+ frontend apps that share code
Bad signals:
You have one app and no immediate plans for more
Your team has fewer than 5 developers
No one on the team has set up a monorepo before (steep learning curve without guidance)
Your CI pipeline isn't already working well (monorepos magnify existing CI problems)
The honest advice: don't start with a monorepo on a new project. Start with a well-structured single-repo app. Move to a monorepo when the pain of shared code management becomes real and measurable.
Week 4 wrap-up
This week we covered:
Day 16 — Folder structure: feature-based vs type-based, path aliases, barrel files
Day 17 — Advanced component patterns: compound components, render props, HOCs
Day 18 — Custom hooks: what belongs in a hook vs component vs utility
Day 19 — API layer: apiClient, error normalisation, services, React Query integration
Day 20 — Monorepos: what they solve, Turborepo basics, real costs
Next week: Performance & Scalability — CSR vs SSR vs SSG, code splitting, virtualisation, bundle optimisation. The week where we make it fast.

See you Monday for Day 21 — CSR vs SSR vs SSG vs ISR: which rendering pattern and when.

← Day 19 — Designing Your API Layer → Day 21 — CSR vs SSR vs SSG vs ISR
Part of the React System Design Series — 40 days, one topic per day.






