Monorepos with pnpm Workspaces & Turborepo: Multi-Project Code Organization

Monorepos with pnpm Workspaces & Turborepo: Multi-Project Code Organization

The Polyrepo Synchronization Trap: How Multi-Repo Silos Cripple Velocity

As digital products evolve beyond a simple frontend into modern ecosystems—combining a customer web application, an administrative dashboard, a shared UI design system, and backend microservices—engineering leaders face a crucial architectural fork in the road:

Do we create separate Git repositories for each project (Polyrepo), or do we unify them under a single, cohesive repository (Monorepo)?

Early on, the polyrepo approach feels intuitively tidy. Teams create company/web-client, company/admin-portal, and company/ui-components.

However, within six months, the polyrepo structure morphs into a chronic synchronization nightmare:

  1. The Shared Component Bottleneck: An engineer updates a button component in ui-components. To use it in web-client, they must commit the code, wait for an NPM package build, publish version 1.4.2 to an internal registry, switch repositories to web-client, update package.json, and run npm install.
  2. Version Drift and Contract Mismatch: The administrative portal uses ui-components@1.2.0, while the customer portal uses 1.4.2. Visual consistency diverges across the company’s products.
  3. Multi-Repo Pull Request Hell: Renaming an API parameter requires three synchronized PRs across three distinct repositories, making atomic rollbacks and unified continuous integration impossible.

THE POLYREPO REPOSITORY SYNCHRONIZATION TRAP

    1. Update UI Library → Commit → Publish to npm (@company/ui v1.4.2)
  1. Web App: Upgrade package.json (Runs v1.4.2)
  2. Admin App: Forgotten! (Stuck on v1.2.0 - Broken UI!)

To dismantle this friction while adhering to anti-over-engineering principles, modern software organizations unify their stack using pnpm Workspaces paired with Turborepo.

Historically, developers resisted monorepos because running npm install inside a multi-project codebase resulted in gigantic 4GB node_modules folders, slow dependency resolution, and severe disk exhaustion.

pnpm solves this at the operating system filesystem level. Instead of duplicating duplicate copies of React or TypeScript across five projects, pnpm maintains a single Global Content-Addressable Store on your machine. When a workspace declares a dependency, pnpm creates a hard link pointing to the shared index:

PNPM HARD-LINK CONTENT-ADDRESSABLE STORE

  • Global Content Store: ~/.local/share/pnpm/store/v3/files/…
  • (Hard Link) — (Hard Link)
  • /apps/web/node_modules (Zero duplicate storage)
  • /apps/admin/node_modules (Zero duplicate storage)

Furthermore, pnpm eliminates phantom dependencies. With legacy flat node_modules, package A could import package B simply because package C had installed it, leading to silent production runtime crashes when package C updated. pnpm uses strict symlink isolation: code can only import what is explicitly declared in its local package.json.

Structuring a Production Monorepo with pnpm Workspaces

Here is an enterprise-grade directory structure for a full-stack TypeScript platform:

  • my-platform/
  • apps/
    • web/ — Customer Portal (Next.js / Astro)
    • admin/ — Admin Operations Dashboard (React)
    • api/ — Backend REST/GraphQL Engine (Node.js)
  • packages/
    • ui/ — Shared Tailwind UI Component Library
    • database/ — Prisma / Kysely Database Models
    • typescript-config/ — Shared tsconfig.json base files
    • eslint-config/ — Unified linting rules
  • pnpm-workspace.yaml
  • turbo.json
  • package.json

We configure pnpm-workspace.yaml at the root to bind applications and internal packages together:

# pnpm-workspace.yaml
packages:
  - "apps/*"
  - "packages/*"

Inside apps/web/package.json, we reference our internal packages using the dynamic workspace:* protocol:

{
  "name": "web",
  "version": "1.0.0",
  "private": true,
  "dependencies": {
    "@company/ui": "workspace:*",
    "@company/database": "workspace:*"
  }
}

When you edit a component inside packages/ui, hot-module reloading in apps/web reflects the change in sub-second time without needing an intermediate compilation or npm publish step! This velocity powers our development of web apps and fast static websites.

Orchestrating Builds with Turborepo: Remote Caching & Task Graphs

While pnpm handles dependency linking, executing builds, tests, and lints across twelve interconnected packages requires intelligent task orchestration.

Running pnpm -r build naively compiles packages sequentially or runs into circular dependency deadlocks. Turborepo solves this by constructing a Directed Acyclic Graph (DAG) of your workspace.

Create turbo.json at the root of the repository:

{
  "$schema": "https://turbo.build/schema.json",
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**", ".next/**", "!.next/cache/**"]
    },
    "test": {
      "dependsOn": ["build"],
      "inputs": ["src/**/*.tsx", "src/**/*.ts", "test/**/*.ts"]
    },
    "lint": {},
    "dev": {
      "cache": false,
      "persistent": true
    }
  }
}

Notice the critical "dependsOn": ["^build"] topological sort: Turborepo automatically figures out that packages/ui must build before apps/web, and builds non-dependent packages concurrently across all CPU cores.

TURBOREPO DIRECTED ACYCLIC GRAPH (DAG)

  • typescript-config eslint-config
  • packages/ui packages/database
  • (Parallelized)
  • apps/web | apps/admin | apps/api

Even better, Turborepo hashes inputs (source code, environment variables, dependencies) against outputs. If you make a one-line change to apps/web, Turborepo calculates that apps/admin and apps/api have not changed. When CI runs turbo run build, it outputs:

apps/admin:build: cache hit, replaying logs from cache (0.04s)
apps/api:build: cache hit, replaying logs from cache (0.03s)
apps/web:build: building... (12.4s)

Build times drop from 15 minutes down to 30 seconds. Paired with Docker multi-stage builds via Docker, deployment pipelines stay blazing fast.

Production Best Practices: CI Pruning, Versioning, and Codeowners

Deploying enterprise monorepos according to the software engineering standards of WebXtek requires following three fundamental best practices:

  1. Leverage turbo prune for Docker Deployments: When building a Docker container for apps/api, you do not want to copy the entire 2GB monorepo into the build context. Run turbo prune --scope=api --docker to isolate only the exact source files and dependencies required by that specific app.
  2. Automate Versioning with Changesets: Never manually edit versions in twenty package.json files. Use @changesets/cli to prompt developers for semver descriptions during PR creation and automate cross-package changelogs.
  3. Enforce GitHub CODEOWNERS: As your engineering team grows, define granular file-based ownership boundaries in .github/CODEOWNERS:
    /apps/web/           @frontend-team
    /apps/api/           @backend-team
    /packages/database/  @data-architects

Monorepo architecture with pnpm workspaces and Turborepo eliminates the friction of polyrepos, unites your engineering team around shared contracts, and scales build execution with mathematical caching efficiency. Reference configurations can be audited on GitHub.

In practice, we apply these exact architectural standards across specialized sectors including b2b saas platforms and devops & cloud architecture specialists. Whether you are modernizing core workflows through web apps and fast static websites that score 95+ on pagespeed, connecting sector-specific requirements to disciplined execution is what turns technical theory into compounding commercial advantage.

[+] [ INDEX_DIRECTORY ] 13 topics ▼
[ SYSTEM.FAQ ]

Frequently Asked Questions

What is the primary advantage of a monorepo over multiple separate Git repositories (polyrepo)?

A monorepo consolidates related applications and shared libraries into a single version-controlled repository. It allows atomic cross-package commits, eliminates version drift between frontend and backend contracts, and makes refactoring shared TypeScript types instantaneous.

How do pnpm workspaces eliminate disk bloat and phantom dependencies?

pnpm uses content-addressable hard links to store all package versions once globally on disk. Furthermore, its symlinked node_modules structure strictly prevents phantom dependencies, ensuring packages can only import dependencies explicitly declared in their own package.json.

What does Turborepo contribute to a pnpm workspace?

Turborepo acts as a high-performance build orchestration engine. It analyzes dependency task graphs (DAGs) to execute builds and tests in parallel with maximum concurrency, and caches build artifacts remotely and locally so unchanged packages never recompile.

When should an engineering team avoid using a monorepo?

Monorepos introduce organizational complexity if teams have completely independent release lifecycles, require strict siloed code access permissions, or lack tooling like Turborepo to prevent CI/CD pipelines from building the entire repository on every commit.

> START_PROJECT

Need a website that earns trust, ranks in search, and gives your business a stronger digital presence? Start the conversation here.