
Managing multiple projects across multiple repositories gets messy fast. This post breaks down what monorepos are, the problems they solve, the new problems they create, and how Turborepo fixes them - without writing a single line of setup code.
Imagine you are building a house. You have a contractor for the foundation, a separate contractor for the walls, and a third one for the roof. Each of them works from a different blueprint, uses different tools, and communicates through a game of telephone. When the foundation team makes a change, the wall team does not know about it until something breaks. Sound exhausting? That is exactly what managing multiple separate code repositories feels like on a real project.
Now imagine a single lead architect who has every blueprint in one room, knows how all the pieces connect, and can coordinate everyone at once. That is the monorepo idea in a single sentence.
Most developers start with one repo per project. You have a frontend repo, a backend repo, maybe a shared utilities repo. This feels clean in the beginning. Each project lives in its own world.
But as things grow, cracks start to appear.
github.com/atharvdange618/ ├── my-app-frontend (React / Next.js) ├── my-app-backend (Express API) └── my-app-shared-utils (TypeScript types, helpers)
Here is the thing: these three projects are not independent. They are deeply connected. The frontend calls the backend. Both the frontend and backend use the same TypeScript types. When you change a type in shared-utils, you need to:
shared-utils to npm (or manage it locally somehow)frontendbackendnpm install in both placesFor a one-person side project, this is annoying. For a team of five, it is a productivity killer. And this is the problem monorepos solve.
A monorepo (short for monolithic repository) is a single git repository that holds multiple related projects. Instead of spreading your code across many repos, everything lives together under one roof.
Think of it like an apartment building vs. separate houses. Separate houses means separate electricity bills, separate maintenance, separate everything. An apartment building shares infrastructure. The pipes, wiring, and elevator are all shared. Each apartment is still its own space, but the common stuff is managed once.
my-monorepo/ <-- single git repository ├── apps/ │ ├── web/ <-- Next.js frontend │ └── api/ <-- Express backend ├── packages/ │ ├── types/ <-- shared TypeScript types │ └── utils/ <-- shared utility functions ├── package.json └── pnpm-workspace.yaml
Every "project" is still its own thing with its own package.json, its own dependencies, its own scripts. But they all live in the same repository, so sharing code between them is just an import away. No publishing, no versioning, no synchronization headaches.
The benefits are real and immediate:
@myapp/types as if it were a regular dependency, but it is just a local folder.Monorepos sound like a dream. And mostly, they are. But they come with one significant growing pain: speed.
When everything lives together, your build and test commands run across everything. Change one line in packages/utils and your CI pipeline tries to rebuild the entire monorepo from scratch. That means rebuilding the frontend, the backend, running all the tests, linting everything. Even the parts that have absolutely nothing to do with your change.
You changed one file in packages/utils | v CI runs build on EVERYTHING ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ apps/web │ │ apps/api │ │ pkg/utils │ │ (rebuilt) │ │ (rebuilt) │ │ (rebuilt) │ └─────────────┘ └─────────────┘ └─────────────┘ Time taken: painfully long
For a small repo, this is fine. For a large one with ten apps and fifteen packages, this is a serious problem. Builds that take 20 minutes when they should take 2.
This is exactly the problem Turborepo was built to fix.
Turborepo is a build system for monorepos. It does not replace pnpm workspaces or your existing tools. It sits on top of everything and makes it dramatically faster.
It does this through two core ideas.
First: it understands your dependency graph.
Turborepo knows which packages depend on which. If apps/web depends on packages/ui, and packages/ui depends on packages/types, Turborepo builds them in the right order automatically. You do not have to think about it.
Dependency Graph (Turborepo figures this out for you): packages/types | v packages/ui ───────┐ | | v v apps/web apps/api Build order: types --> ui --> web & api (in parallel)
Second: it caches everything.
This is the real magic. When Turborepo runs a task like build on a package, it stores the result in a cache. The next time you run the same task and nothing has changed in that package, it just replays the cached output. Instantly. No rebuild.
First run: packages/types --> [BUILD] --> cache saved packages/ui --> [BUILD] --> cache saved apps/web --> [BUILD] --> cache saved Second run (you only changed apps/web): packages/types --> [CACHE HIT] ✓ (skipped) packages/ui --> [CACHE HIT] ✓ (skipped) apps/web --> [BUILD] ✓ (rebuilt, because it changed)
The cache can even be shared across your entire team through Vercel's Remote Cache. That means when your teammate builds the project and pushes the cache, your machine benefits from their build on the next pull. Your CI pipeline benefits too.
Before Turborepo, caching in monorepos was mostly nonexistent. Lerna - the most popular monorepo tool at the time - had no caching at all; every build was a full rebuild. Some teams hacked together Makefiles with manual hash checks. Others adopted Nx, which did have caching but came with its own paradigm shift and ecosystem lock-in. Most teams simply accepted the slow builds and moved on. Turborepo was the first tool to make caching both automatic and painless - install it, define your task pipeline, and it just works.
Put simply: pnpm workspaces give you the monorepo structure. Turborepo gives you the speed to actually work in it without wanting to throw your laptop out the window.
Monorepos are not a silver bullet. They solve real problems, but they also add real complexity. Here is the honest picture.
You probably do not need a monorepo if:
Jumping into a monorepo as your first project setup adds a layer of tooling that you do not yet need. Learn how npm packages, git, and project structure work first. The monorepo will make more sense when you have actually felt the pain it solves.
You should seriously consider a monorepo when:
One repo, no shared code: --> separate repos, keep it simple One repo, lots of shared code: --> monorepo is the right call Many unrelated projects: --> separate repos, no question Team with shared types/utils: --> monorepo saves everyone time
The rule of thumb is this: reach for a monorepo when the cost of keeping multiple repos in sync is higher than the cost of learning the monorepo tooling. For most full-stack JavaScript projects on a team of two or more, that point comes pretty quickly.
Monorepo is a single git repository that holds multiple related projects. It solves the code sharing and synchronization problems that come with separate repos.
The trade-off is speed. Monorepos get slow as they grow because everything gets rebuilt together.
Turborepo solves the speed problem by understanding your dependency graph and caching build outputs so unchanged packages are never rebuilt twice.
Do not reach for a monorepo too early. It is a solution to a specific set of problems. When you have those problems, it is a fantastic tool. When you do not, it is unnecessary complexity.
The biggest shift in mindset is this: you are no longer thinking in terms of individual projects. You are thinking in terms of a system. Packages are building blocks. Apps are assembled from those blocks. Turborepo makes sure the whole thing builds fast. That mental model, once it clicks, makes working in large codebases genuinely enjoyable.
Ready to actually build one? Read Setting Up a Monorepo with pnpm and Turborepo for a step-by-step setup guide including workspace configuration, shared packages, and common beginner mistakes.
Related posts based on tags, category, and projects
You understand why monorepos exist and what Turborepo does. Now let us actually build one. This guide walks through setting up pnpm workspaces, creating shared packages, wiring up a Next.js frontend and Express backend, adding Turborepo for caching, and avoiding the common pitfalls.
Middleware is code that runs between a request arriving and a response being sent. In Express, every middleware function in the chain gets a chance to inspect, modify, or stop a request. This post covers what middleware is, how `next()` controls the flow, and where it gets used in real applications.
Node.js isn't fast because of raw processing power. It's fast because it never waits around when there's work to do. This post covers the architectural decisions that make Node.js well-suited for high-concurrency web applications.
REST is a set of conventions for designing APIs that are predictable, consistent, and easy to work with. This post covers what REST means, how it maps to HTTP, and how to build a clean resource-based API in Express.