What Is a Monorepo and When Should You Use One for JavaScript Projects?
A monorepo is a single version-controlled repository that holds the source code for multiple projects, packages, or services. Instead of giving each package its own repository (a "polyrepo" setup), you keep them together and manage their relationships in one place. For JavaScript and TypeScript teams, the appeal is usually straightforward: shared code becomes easier to reuse, changes that span several packages can land in one commit, and dependency versions stop drifting apart. The cost is equally real — you take on more build and CI complexity, and you need tooling that understands which packages changed and what depends on what.
This article explains the model, compares it with polyrepos, and gives you concrete criteria for deciding whether it fits your project.
Monorepo vs. polyrepo: the core difference
The distinction is not about how many packages you have. It is about where the boundaries sit between "one repository" and "many repositories," and what those boundaries force you to do.
| Dimension | Monorepo | Polyrepo |
|---|---|---|
| Code location | All packages in one repo | One repo per package/service |
| Cross-package change | One commit, one review, one CI run | Multiple commits across repos, coordinated releases |
| Shared code | Direct import via workspace links | Published package, version bump, or git submodule |
| Versioning | Often unified or independently managed in one place | Naturally independent, but easy to drift |
| Access control | Repo-level; finer control needs extra tooling | Per-repo permissions out of the box |
| CI scope | Needs change detection to avoid rebuilding everything | Each repo's CI is naturally scoped |
| Onboarding | One clone, one install | Clone only what you need |
| Repo size | Grows with every package | Stays small per repo |
Neither column is universally better. A monorepo trades isolation for coordination; a polyrepo trades coordination for isolation.
What a monorepo actually gives you
Shared code without publishing overhead
In a polyrepo, reusing an internal utility means publishing it, bumping its version, and updating consumers. In a monorepo with a workspace setup (npm, Yarn, or pnpm workspaces), packages can reference each other directly. You edit the shared package and its consumers in the same working tree.
Atomic, cross-package changes
If a change to a shared type breaks three consumers, a monorepo lets you fix all four in one pull request. Reviewers see the full blast radius. In a polyrepo, that same change becomes a sequence of releases where the intermediate states can be broken.
Consistent tooling and dependencies
One lint config, one TypeScript config base, one test runner setup. You can still allow per-package overrides, but the default is consistency, which reduces "it works in my package" problems.
Easier refactoring and discovery
Renaming a function or moving a module across package boundaries is a single search-and-replace plus a build. You can also see every consumer of an internal API, which makes deprecation decisions less risky.
The trade-offs you should expect
Build and task orchestration becomes a real problem
Once you have dozens of packages, running every test and build on every change is wasteful. You need tooling that can:
- Detect which packages changed since a given commit or branch.
- Build a dependency graph and run tasks in the correct order.
- Cache results so unchanged packages are skipped.
This is the layer that tools like Lerna, Nx, Turborepo, and similar systems address. The specific choice matters less than the capability: change detection, task graph, and caching.
CI cost and time
Without caching and affected-only runs, CI in a monorepo can be slower and more expensive than in a polyrepo, because a naive pipeline rebuilds everything. With them, it is often comparable or better, since you avoid re-running unchanged work.
Access control and ownership
A monorepo makes it harder to say "only the payments team can merge to the payments service." You can approximate this with CODEOWNERS files and branch protection rules, but it is not the same as separate repositories. If strict isolation is a hard requirement, weigh this carefully.
Repository size and clone time
Large monorepos get big. Shallow clones, sparse checkouts, and partial clone features help, but they add setup steps for contributors.
Release management
You must decide between unified versioning (all packages share a version) and independent versioning (each package versions on its own). Unified is simpler to reason about; independent is more flexible but requires discipline and tooling to keep changelogs and dependency ranges correct.
When a monorepo fits well
Consider a monorepo when most of these are true:
- You have multiple packages that genuinely depend on each other and change together.
- You want to enforce shared standards (linting, TypeScript config, testing) across teams.
- Your team is willing to invest in build tooling and CI caching.
- You value atomic changes across package boundaries more than strict per-repo isolation.
- You are building a product with a shared design system, shared types, or a shared SDK alongside apps.
A common example: a web app, a mobile app, and a shared component library that all evolve together. A change to a shared button component should be verifiable against both apps in one place.
When a monorepo does not fit
A polyrepo is often the better choice when:
- Packages are truly independent and rarely change together.
- Different teams need hard access boundaries for compliance or security reasons.
- Packages have very different release cadences and you do not want to coordinate them.
- The repository would become so large that clone and CI times hurt daily work, and you are not prepared to invest in tooling.
- You are experimenting with unrelated projects that share nothing but a language.
A useful rule of thumb: if you would never make a single commit that touches two of the packages, the coordination benefit of a monorepo is small.
A practical way to decide
- List your packages and draw the dependency edges between them. Count how many edges cross what would be repository boundaries.
- Estimate how often a change touches more than one package. If it is frequent, a monorepo helps; if it is rare, it mostly adds overhead.
- Check your CI budget and willingness to adopt caching and affected-only task running.
- Confirm your access-control requirements. If per-package permissions are mandatory, plan for CODEOWNERS and branch rules, or reconsider.
- Start small if you proceed: put two or three related packages in one repo first, set up workspace linking, and add change detection before the package count grows.
Where tooling fits in
A monorepo is a repository layout, not a tool. What makes it practical at scale is the build system around it: workspace linking for local dependencies, a task runner that understands the dependency graph, caching to skip unchanged work, and a publishing flow that handles versioning and changelogs. Lerna, for example, is a build system for managing and publishing multiple JavaScript/TypeScript packages from the same repository, and it is often used alongside or in combination with task runners like Nx. The right question is not "which tool is best" but "does my setup detect changes, order tasks correctly, and cache results?" If yes, the monorepo stays manageable as it grows.
Summary
A monorepo puts multiple packages in one repository to make shared code, atomic changes, and consistent tooling easier. It pays off when packages genuinely depend on each other and your team invests in change detection, task orchestration, and CI caching. It costs you isolation, access-control granularity, and setup complexity. Decide by measuring how often changes cross package boundaries and how much coordination pain you currently feel — not by whether monorepos are fashionable.