Managing Monorepo Dependencies with Turborepo
Managing shared dependencies across monorepo packages requires a combination of native package manager workspaces and Turborepo’s task orchestration. While package managers like pnpm, Yarn, or npm handle dependency hoisting, version resolution, and linking, Turborepo optimizes the build pipeline by understanding dependency graphs and caching artifacts. This guide explains how development teams structure their workspaces, synchronize third-party libraries, handle internal packages, and leverage Turborepo to eliminate redundant work.
The Separation of Concerns: Workspaces vs. Turborepo
To manage shared dependencies effectively, teams must understand that Turborepo is not a package manager. It does not install packages, resolve dependency trees, or hoist node modules. Instead:
- The Package Manager (pnpm, Yarn, npm): Responsible
for installing external dependencies, hoisting shared packages to the
root
node_modules, and symlinking internal packages together via workspace protocols. - Turborepo: Reads the package dependency graph
established by the package manager. It uses
turbo.jsonto orchestrate build, lint, and test pipelines in the correct topological order and caches the outputs.
Structuring Internal Shared Packages
Teams typically organize internal shared code—such as UI libraries,
ESLint configurations, TypeScript configs, and utility functions—into
dedicated packages within a packages/ directory.
Internal packages are assigned their own package.json
with a unique name (often scoped, like @repo/ui or
@repo/utils). Consuming applications within the monorepo
(such as @repo/web) declare internal packages in their
dependencies using workspace protocols:
{
"name": "@repo/web",
"dependencies": {
"@repo/ui": "workspace:*",
"@repo/utils": "workspace:*"
}
}The workspace:* protocol (supported by pnpm and Yarn)
ensures that the consuming app always links to the local version of the
code inside the monorepo, rather than looking for a published package on
an external registry.
Enforcing Pipeline Order with Turborepo
When one package depends on another, Turborepo must build them in the
correct sequence. Teams configure this relationship in the root
turbo.json file using the dependsOn property
with the ^ prefix:
{
"$schema": "https://turbo.build/schema.json",
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", ".next/**"]
}
}
}The ^build task tells Turborepo that before running
build for any package, it must first successfully run
build for all internal dependencies of that package.
Turborepo builds these dependencies in parallel whenever possible,
caches the build artifacts, and skips rebuilding them in subsequent runs
if the underlying files have not changed.
Synchronizing External Dependency Versions
A common challenge in monorepos is version drift, where different
packages rely on conflicting versions of external packages, such as
react or typescript. Teams resolve this using
specific management strategies:
- Strict Root Catalogs (pnpm Catalogs): Modern tools like pnpm provide catalogs, allowing teams to define shared dependency versions once in a root configuration and reference them across child packages without hardcoding version strings.
- Linting Dependencies with Syncpack: Teams integrate
tools like
syncpackinto their CI pipelines. Syncpack inspects allpackage.jsonfiles in the monorepo, alerts developers when mismatched versions exist, and automatically aligns them. - Peer Dependencies for Shared UI: For internal
design systems or shared UI libraries, teams define common runtimes
(like
reactandreact-dom) aspeerDependencies. This avoids bundling duplicate runtime instances into client applications.
Just-in-Time (JIT) Compilation
To speed up internal dependency management, modern teams often skip
the build step for internal packages altogether. Instead of compiling
shared UI or utilities to a dist/ folder, internal packages
export raw TypeScript source files directly through the
package.json exports field:
{
"name": "@repo/ui",
"exports": {
"./button": "./src/button.tsx"
}
}The consuming framework (such as Next.js or Vite) compiles the raw source files on demand during the application build. This eliminates the need for separate compile steps for internal packages, significantly reducing Turborepo pipeline complexity and local iteration times.