@microsoft/rush, lerna, nx, and turbo are all tools designed to manage JavaScript/TypeScript monorepos—codebases that contain multiple packages or applications in a single repository. They address challenges like dependency hoisting, cross-package linking, task orchestration, and build optimization. While they share overlapping goals, their architectures, philosophies, and feature sets differ significantly. Rush emphasizes strict dependency management and enterprise-scale workflows; Lerna pioneered the space with simple package publishing and linking; Nx provides deep project graph analysis and computation caching with strong support for full-stack development; Turbo (from Vercel) focuses on high-speed incremental builds and remote caching with minimal configuration.
Managing a JavaScript monorepo isn’t just about putting multiple projects in one folder—it’s about solving hard problems: consistent dependencies, efficient builds, safe publishing, and developer velocity at scale. Let’s dissect how @microsoft/rush, lerna, nx, and turbo tackle these challenges.
@microsoft/rush uses its own install engine (wrapping PNPM by default) to enforce single-version policy: every package in the repo must use the same version of any given dependency. This eliminates subtle bugs from version skew but requires careful coordination during upgrades.
// rush.json
{
"pnpmVersion": "8.7.0",
"ensureConsistentVersions": true,
"projects": [
{ "packageName": "@myorg/ui", "projectFolder": "packages/ui" },
{ "packageName": "@myorg/api", "projectFolder": "apps/api" }
]
}
lerna delegates entirely to the underlying package manager (npm/yarn/pnpm). It doesn’t enforce version consistency—each package can have its own node_modules tree unless you enable useWorkspaces. This flexibility often leads to diamond dependency issues in practice.
nx is agnostic—it works atop npm, Yarn, or PNPM workspaces. However, it provides dependency constraints via nx.json to enforce rules like “libs cannot depend on apps”:
// nx.json
{
"targetDefaults": {
"build": { "dependsOn": ["^build"] }
},
"dependencyConstraints": {
"enforceBuildableLibDependency": true
}
}
turbo assumes you’re using npm/Yarn/PNPM workspaces and doesn’t interfere with dependency resolution. It focuses purely on task execution, not dependency hygiene.
rush defines tasks via command-line.json, supporting phased execution and custom parameters:
// common/config/rush/command-line.json
{
"commands": [
{
"commandKind": "bulk",
"name": "build",
"summary": "Build all projects",
"safeForSimultaneousRushProcesses": true
}
]
}
It runs tasks per-project but lacks deep awareness of what changed—you get parallelism, not intelligence.
lerna offers basic parallel/concurrent execution:
lerna run build --parallel
But it has no concept of task outputs or caching—every run rebuilds everything unless you script around it.
nx builds a project graph from source code imports and package.json dependencies. This enables nx affected:build to run only what’s needed:
# Only build projects affected by the last commit
nx affected:build --base=HEAD~1 --head=HEAD
Nx also supports computation caching—locally and via Nx Cloud—so identical inputs skip execution entirely.
turbo uses a pipeline definition to model task dependencies and cache outputs based on file hashes:
// turbo.json
{
"pipeline": {
"build": {
"outputs": ["dist/**"],
"dependsOn": ["^build"]
},
"test": {
"outputs": [],
"dependsOn": ["build"]
}
}
}
Run with turbo run build—it skips cached tasks and executes others in topological order.
rush has no built-in caching. You’d need to layer in something like Azure Artifacts or custom scripts.
lerna also lacks caching—every task runs from scratch.
nx caches based on source files, environment variables, and task configuration. The cache includes terminal output and artifacts. With Nx Cloud, teams share a remote cache:
# Enable remote caching
nx connect-to-nx-cloud
turbo caches task outputs (defined in turbo.json) using content hashes of inputs (source files, env vars, global hash). Remote caching is built-in via Vercel’s infrastructure:
# Login to enable remote cache
npx turbo login
Both Nx and Turbo invalidate correctly on code changes—but Nx’s project graph gives it finer granularity (e.g., skipping tests for unchanged libs).
rush excels here with change files—developers describe changes via rush change, then rush version auto-bumps versions and updates changelogs. Publishing is atomic and auditable:
rush change --type minor --message "Add dark mode"
rush version
rush publish --include-all
lerna popularized lerna version and lerna publish, but its fixed/independent modes are error-prone without additional tooling. No built-in change tracking.
nx doesn’t handle publishing natively—you’d use nx release (experimental) or integrate with Changesets or manually script it.
turbo has no publishing features. It’s purely a build orchestrator.
rush demands upfront investment: config files live in common/config/rush, and you must adopt its conventions. Great for enterprises, overkill for small teams.
lerna feels familiar but dated—its simplicity is now a liability as monorepo needs evolve.
nx offers generators (nx g @nx/react:lib ui) and lint rules that understand your project graph. Plugins exist for every major framework, and the VS Code extension shows dependency visualizations.
turbo wins on zero-config adoption—add turbo.json and you’re off. But you trade configurability for speed; no code generators, no dependency linting.
Despite differences, all four tools converge on key principles:
Each understands that a monorepo contains multiple logical projects and can run scripts across them:
# Common pattern: run test in all packages
rush test # Rush
lerna run test # Lerna
nx run-many -t test # Nx
turbo run test # Turbo
All support concurrent task execution to leverage multi-core machines:
# Example: build all projects in parallel
rush build --parallel
lerna run build --parallel
nx run-many -t build --parallel
They assume you’re using npm/Yarn/PNPM workspaces under the hood (except Rush, which wraps PNPM directly).
Even Lerna (via --since) tries to limit scope—though only Nx and Turbo do it robustly.
| Feature | @microsoft/rush | lerna | nx | turbo |
|---|---|---|---|---|
| Dependency Policy | Enforces single-version | No enforcement | Constraints via config | No enforcement |
| Task Intelligence | Basic parallelism | Basic parallelism | Project graph + affected | Pipeline + file hashing |
| Caching | None | None | Local + remote (Nx Cloud) | Local + remote (Vercel) |
| Publishing | Built-in (change files) | Built-in (basic) | Experimental / manual | None |
| Code Generation | No | No | Yes (via plugins) | No |
| Best For | Enterprise-scale, strict control | Legacy repos, simple publishing | Full-stack apps, DX-focused | Speed-focused, minimal config |
The Big Picture:
Choose based on whether your bottleneck is correctness (Rush), velocity (Nx), or raw speed (Turbo).
Choose @microsoft/rush if you're operating at enterprise scale with strict requirements around dependency consistency, deterministic installs, and CI reproducibility. Rush enforces a single version policy for dependencies across projects and integrates tightly with PNPM via its own installer wrapper. It’s ideal for large teams needing auditability, change logs, and safe publishing workflows—but be prepared for higher configuration overhead and less flexibility in toolchain choices.
Lerna is best suited for smaller or legacy monorepos where the primary needs are basic package linking and versioned publishing. While it once dominated the space, its core functionality has been largely superseded by more modern tools. Avoid using Lerna for new projects unless you specifically require its lightweight publishing model and don’t need advanced features like computation caching or deep task orchestration. For most greenfield efforts, consider Nx or Turbo instead.
Choose Nx when you need intelligent task orchestration powered by a fine-grained project graph, extensive plugin ecosystem (React, Angular, NestJS, etc.), and local/remote computation caching. Nx shines in full-stack monorepos with interdependent apps and libs, offering features like affected commands, code generation, and distributed task execution. Its deep integration with modern frameworks and developer ergonomics make it a strong choice for teams prioritizing DX and long-term maintainability.
Opt for turbo when your top priority is build speed through incremental execution and remote caching with minimal setup. Turbo’s zero-config approach (especially with Turborepo) works well for teams already using npm or Yarn workspaces and wanting fast CI without complex pipeline definitions. It’s particularly effective in Vercel-hosted environments but lacks built-in code generation, dependency constraints, or deep framework integrations found in Nx.
Rush makes life easier for JavaScript developers who build and publish many NPM packages at once. If you're looking to consolidate all your projects into a single repo, you came to the right place! Rush is a fast, professional solution for managing this scenario. It gives you:
A single NPM install: In one step, Rush installs all the dependencies for all your projects into a common folder. This is not just a "package.json" file at the root of your repo (which might set you up to accidentally require() a sibling's dependencies). Instead, Rush uses symlinks to reconstruct an accurate "node_modules" folder for each project, without any of the limitations or glitches that seem to plague other approaches.
⏵ This algorithm supports the PNPM, NPM, and Yarn package managers.
Automatic local linking: Inside a Rush repo, all your projects are automatically symlinked to each other. When you make a change, you can see the downstream effects without publishing anything, and without any npm link headaches. If you don't want certain projects to get linked, that's supported, too.
Fast builds: Rush detects your dependency graph and builds your projects in the right order. If two packages don't directly depend on each other, Rush parallelizes their build as separate Node.js processes (and shows live console output in a readable order). In practice this multi-process approach can yield more significant speedups than all those async functions in your single-threaded Gulpfile.
Subset and incremental builds: If you only plan to work with a few projects from your repo, rush rebuild --to <project> does a clean build of just your upstream dependencies. After you make changes, rush rebuild --from <project> does a clean build of only the affected downstream projects. And if your toolchain is package-deps-hash enabled, rush build delivers a powerful cross-project incremental build (that also supports subset builds).
Cyclic dependencies: If you have hammers that build hammer-factory-factories, Rush has you covered! When a package indirectly depends on an older version of itself, projects in the cycle use the last published version, whereas other projects still get the latest bits.
Bulk publishing: When it's time to do a release, Rush can detect which packages have changes, automatically bump all the appropriate version numbers, and run npm publish in each folder. If you like, configure your server to automatically run rush publish every hour.
Changelog tracking: Whenever a PR is created, you can require developers to provide a major/minor/patch log entry for the affected projects. During publishing, these changes will be automatically aggregated into a nicely formatted CHANGELOG.md file.
Enterprise policies: Want to review new libraries before developers add them to package.json, but avoid hassling people about already approved cases? Want to enforce that all your projects depend on the same library version numbers? Are unprofessional personal e-mail addresses accidentally showing up in your company's Git history? Rush can help maintain a consistent ecosystem when you've got many developers and many projects in the mix.
Lots more! Rush was created by the platform team for Microsoft SharePoint. We build hundreds of production NPM packages every day, from internal and public Git repositories, for third party SDKs and live services with millions of users. If there's an important package management problem that needs solvin', it's likely to end up as a feature for Rush.
See Rush in action! From your shell, install the tool like this:
$ npm install -g @microsoft/rush
For command-line help, do this:
$ rush -h
To see Rush build some real projects, try running these commands: :-)
$ git clone https://github.com/microsoft/rushstack
$ cd rushstack
$ rush install
$ rush install # <-- instantaneous!
$ rush rebuild
$ rush build # <-- instantaneous!
(If you don't have a GitHub account set up, you can use rush install --bypass-policy.)
For more details and support resources, please visit: https://rushjs.io
Rush is part of the Rush Stack family of projects.