Skip to main content

JavaScript Monorepos in 2026: Best Practices and Pitfalls

·PkgPulse Team

TL;DR

Turborepo + pnpm for most monorepos; Nx for enterprise-scale. Turborepo (~2M weekly downloads) became the default monorepo build orchestrator in 2026 — simple config, fast caching, works with any package manager. pnpm workspaces (~5M downloads) is the standard package manager for monorepos. Nx (~2M) is the right choice when you need code generation, affected-project detection at enterprise scale, or deep framework integrations. Don't use Lerna in 2026 — it's been superseded.

Key Takeaways

  • Turborepo: ~2M weekly downloads — build orchestration, caching, task dependency graph
  • Nx: ~2M downloads — enterprise-scale, code generators, affected project detection
  • pnpm workspaces: ~5M downloads — workspace management, hoisting control, symlinks
  • The key win — remote caching (Turborepo/Nx) → CI runs that hit cache complete in seconds
  • Common mistake — too granular packages; start with 3-5 packages, not 50

When to Use a Monorepo

# Good reasons for a monorepo:
✅ Multiple apps sharing code (web + api + mobile using same types)
✅ UI component library shared across multiple frontends
✅ Multiple packages published to npm from one repo
✅ Team wants atomic commits (UI + API change in one PR)
✅ Shared tooling configuration (ESLint, TypeScript, Prettier)

# Bad reasons (monorepo overhead not worth it):
❌ Single app with no sharing
❌ "It might grow into needing shared code" (optimize when you have the problem)
❌ Team has no experience with monorepos (significant learning curve)
❌ Shared CI infrastructure isn't set up to handle affected-project detection

# The monorepo threshold:
# If you have 2+ apps sharing code → consider monorepo
# If you have 5+ apps or 3+ published packages → monorepo is likely worth it

Setup: Turborepo + pnpm

# Bootstrap a new Turborepo monorepo
npx create-turbo@latest

# Or add Turborepo to existing pnpm workspace:
pnpm add turbo --workspace-root
// pnpm-workspace.yaml — workspace definition
packages:
  - "apps/*"         # All subdirectories in apps/
  - "packages/*"     # All subdirectories in packages/

# Directory structure:
monorepo/
├── apps/
│   ├── web/          # Next.js web app
│   ├── api/          # Hono API server
│   └── admin/        # Internal admin Next.js app
├── packages/
│   ├── ui/           # Shared React components
│   ├── database/     # Drizzle schema + db client
│   ├── config/       # Shared TS/ESLint/Biome config
│   └── utils/        # Shared TypeScript utilities
├── turbo.json
├── pnpm-workspace.yaml
└── package.json
// turbo.json — build pipeline configuration
{
  "$schema": "https://turbo.build/schema.json",
  "tasks": {
    "build": {
      "dependsOn": ["^build"],    // ^ = run this task in deps first
      "outputs": [".next/**", "dist/**", "build/**"],  // Cache these
      "cache": true
    },
    "dev": {
      "cache": false,             // Never cache dev servers
      "persistent": true          // Keep running (don't exit)
    },
    "lint": {
      "dependsOn": [],            // No deps — run all in parallel
      "outputs": []               // No cacheable output
    },
    "test": {
      "dependsOn": ["^build"],
      "outputs": ["coverage/**"]
    },
    "type-check": {
      "dependsOn": ["^build"],
      "outputs": []
    }
  },
  "remoteCache": {
    "enabled": true               // Vercel remote cache (free tier available)
  }
}
# Running tasks across all packages
turbo build         # Build all packages (in dependency order)
turbo build lint    # Build then lint, in parallel where possible
turbo dev           # Start all dev servers
turbo test --filter=@myapp/web  # Only web app
turbo build --filter=...@myapp/web  # Web app and its deps
turbo build --since=main        # Only changed packages since main

Remote Caching (The Key CI Win)

# Without remote cache:
# PR #1: npm install → build → test → 6 minutes
# PR #2: npm install → build → test → 6 minutes
# (even if only 1 package changed)

# With Turborepo remote cache:
# PR #1: npm install → build → test → 6 minutes (cache primes)
# PR #2: npm install → cache hit (web) → build api (only change) → 45 seconds

# Real CI time savings at scale:
# Before remote cache: 8 min per PR
# After remote cache: 1.5 min per PR (80%+ cache hit rate)
# Savings: ~6.5 min per PR × 50 PRs/day = 5+ hours of CI time/day

# Setup Vercel Remote Cache (free):
npx turbo login
npx turbo link  # Links to Vercel team for cache sharing

Package Configuration

// packages/ui/package.json — shared component library
{
  "name": "@myapp/ui",
  "version": "0.0.0",           // "Private" packages often use 0.0.0
  "private": true,               // Not published to npm
  "main": "./src/index.ts",      // Direct TypeScript source (no build needed!)
  "types": "./src/index.ts",
  "exports": {
    ".": "./src/index.ts"
  },
  "devDependencies": {
    "react": "^18.2.0",          // devDependency: consumers provide react
    "typescript": "^5.3.0"
  },
  "peerDependencies": {
    "react": "^18.2.0"
  }
}

// Note: For monorepo-internal packages, you can skip the build step
// apps/web imports @myapp/ui source TypeScript directly
// tsconfig paths + TypeScript resolves it
// apps/web/package.json — Next.js app
{
  "name": "@myapp/web",
  "private": true,
  "dependencies": {
    "@myapp/ui": "workspace:*",       // pnpm workspace protocol
    "@myapp/database": "workspace:*",
    "@myapp/utils": "workspace:*",
    "next": "15.0.0",
    "react": "18.3.0"
  }
}
// packages/config/package.json — shared configs
{
  "name": "@myapp/config",
  "private": true,
  "main": "index.js",
  "files": ["biome.json", "tsconfig.json", "tsconfig.base.json"]
}

// packages/config/tsconfig.base.json
{
  "compilerOptions": {
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "skipLibCheck": true
  }
}

// apps/web/tsconfig.json
{
  "extends": "@myapp/config/tsconfig.base.json",
  "compilerOptions": {
    "plugins": [{ "name": "next" }],
    "paths": { "@/*": ["./src/*"] }
  }
}

Common Pitfalls

Pitfall 1: Too Many Packages Too Early

# Bad: 25 packages for a 3-developer team
packages/
├── api-client/
├── auth/
├── button/           ← Should be in ui/
├── card/             ← Should be in ui/
├── constants/
├── database/
├── email/
├── env/
├── errors/
├── formatters/       ← utils/
├── hooks/            ← Should be in ui/ or web/
├── icons/
├── input/            ← Should be in ui/
├── logger/
├── modal/            ← Should be in ui/
├── permissions/
├── types/
├── utils/
└── validators/       ← Part of utils or each consumer

# Better: 4-5 packages that grow naturally
packages/
├── ui/           # All shared React components
├── database/     # Schema + db client
├── utils/        # Shared TypeScript utilities
└── config/       # Shared configs (TS, ESLint, etc.)

# Rule: start with 3-5 packages; extract to new package when there's genuine need

Pitfall 2: Version Mismatch Hell

# Problem: apps/web uses React 18.2; packages/ui uses React 18.0
# Solution: declare React as peerDependency in packages, devDependency

# pnpm-workspace.yaml — hoist React to root
# pnpm-workspace.yaml
packages:
  - "apps/*"
  - "packages/*"

# .npmrc — prevent duplicate React instances
dedupe-peer-dependents=true

Pitfall 3: Forgetting --filter in CI

# Bad CI: always build everything
# .github/workflows/ci.yml
- run: turbo build  # Runs ALL packages every PR

# Good CI: only affected packages + cache
- run: turbo build --filter=...[origin/main]
# Only builds packages changed since main branched
# Combined with remote cache: usually finishes in seconds

Nx vs Turborepo Comparison

FeatureTurborepoNx
Setup complexityLowMedium-High
Remote caching✅ Vercel cloud✅ Nx Cloud (paid)
Code generation✅ Generators
Affected detection--filter--affected
Framework plugins❌ Agnostic✅ React, Angular, etc.
CI time at scaleFastFaster (smarter graph)
Learning curveLowHigh
Best forStartups, mid-sizeEnterprise, 50+ packages

Compare monorepo tooling package health on PkgPulse.

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.