Skip to content
All essays
CraftMarch 27, 202513 min

Monorepos: Managing Multiple Projects in One Repository

Master monorepo architecture. Manage multiple projects, share code, and streamline development with modern monorepo tools.

Ü
Ümit Uz
Mobile & Full Stack Developer

Understanding Monorepos

A monorepo is a version control strategy where multiple projects are stored in a single repository. This approach has gained popularity with large companies like Google, Facebook, and Microsoft, along with modern tools like Nx, Turborepo, and pnpm workspaces.

bash
# Monorepo structure
my-monorepo/
├── apps/
│   ├── web/              # Frontend application
│   ├── api/              # Backend API
│   └── mobile/           # Mobile app
├── packages/
│   ├── ui/               # Shared UI components
│   ├── utils/            # Shared utilities
│   └── types/            # Shared TypeScript types
├── package.json
└── turbo.json            # Build orchestration

Monorepo vs Polyrepo

Key Differences

bash
# Monorepo
✓ Single source of truth
✓ Unified CI/CD
✓ Shared dependencies
✓ Atomic commits
✓ Easy code sharing
✗ Larger repository size
✗ Slower git operations
✗ Complex build setup

# Polyrepo
✓ Independent repositories
✓ Faster git operations
✓ Separate deployments
✗ Code duplication
✗ Version synchronization
✗ Complex dependencies

Monorepo Tools

pnpm Workspaces

json
// pnpm-workspace.yaml
packages:
  - 'apps/*'
  - 'packages/*'

// package.json (root)
{
  "name": "my-monorepo",
  "private": true,
  "scripts": {
    "dev": "turbo run dev",
    "build": "turbo run build",
    "test": "turbo run test",
    "lint": "turbo run lint"
  },
  "devDependencies": {
    "turbo": "^1.10.0",
    "typescript": "^5.0.0"
  }
}

// apps/web/package.json
{
  "name": "@myorg/web",
  "dependencies": {
    "@myorg/ui": "workspace:*",
    "@myorg/utils": "workspace:*"
  }
}

Nx Monorepo

bash
# Create Nx workspace
npx create-nx-workspace@latest my-workspace

# Choose stack:
# - Integrated (monorepo with Nx)
# - Standalone (single project)

# Add applications
nx g @nx/react:app my-app

# Add libraries
nx g @nx/react:lib shared-ui

# Structure:
my-workspace/
├── apps/
│   └── my-app/
├── libs/
│   └── shared-ui/
├── nx.json
└── package.json

Turborepo

json
// turbo.json
{
  "$schema": "https://turbo.build/schema.json",
  "globalDependencies": ["**/.env.*local"],
  "pipeline": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**", ".next/**", "!.next/cache/**"]
    },
    "dev": {
      "cache": false,
      "persistent": true
    },
    "test": {
      "dependsOn": ["build"],
      "outputs": ["coverage/**"]
    },
    "lint": {
      "outputs": []
    }
  }
}

// Run tasks
turbo run build    # Build all apps
turbo run dev      # Start dev servers
turbo run test     # Run tests

Project Structure

bash
my-monorepo/
├── apps/                    # Deployable applications
│   ├── web/                 # React app
│   ├── api/                 # Express server
│   └── admin/               # Admin dashboard
├── packages/                # Shared packages
│   ├── ui/                  # UI components
│   ├── utils/               # Utilities
│   ├── config/              # Shared config
│   └── types/               # TypeScript types
├── tools/                   # Build tools
│   ├── eslint-config/
│   └── tsconfig/
├── .github/                 # CI/CD
├── docs/                    # Documentation
├── package.json
├── pnpm-workspace.yaml
├── turbo.json
└── nx.json

Package Naming

json
// Use scoped names for organization
{
  "name": "@myorg/web",
  "version": "1.0.0"
}

// Reference in dependencies
{
  "dependencies": {
    "@myorg/ui": "workspace:*",
    "@myorg/utils": "workspace:*"
  }
}

// Publish to npm
{
  "name": "@myorg/ui",
  "publishConfig": {
    "access": "public"
  }
}

Dependency Management

Internal Dependencies

json
// packages/ui/package.json
{
  "name": "@myorg/ui",
  "dependencies": {
    "@myorg/utils": "workspace:*",
    "@myorg/types": "workspace:*"
  }
}

// apps/web/package.json
{
  "name": "@myorg/web",
  "dependencies": {
    "@myorg/ui": "workspace:*",
    "@myorg/utils": "workspace:*"
  }
}

// pnpm installs and links automatically
pnpm install

External Dependencies

json
// Use package managers' deduplication
{
  "dependencies": {
    "react": "^18.0.0"
  },
  "devDependencies": {
    "typescript": "^5.0.0"
  }
}

// pnpm deduplicates across workspace
// Single copy of react, typescript, etc.
pnpm install --shamefully-hoist  # If needed

Build Orchestration

Task Dependencies

json
// turbo.json
{
  "pipeline": {
    "build": {
      "dependsOn": ["^build"],  // Build dependencies first
      "outputs": ["dist/**"]
    },
    "test": {
      "dependsOn": ["build"],    // After build
      "outputs": ["coverage/**"]
    },
    "dev": {
      "cache": false,            // Don't cache dev mode
      "persistent": true         // Long-running process
    }
  }
}

// Build order automatically determined
// packages/utils → packages/ui → apps/web

Caching Strategy

bash
# Turborepo caching
turbo run build

# First run: Execute all builds
# Second run: Skip unchanged packages
# Cache stored in .turbo/cache

# Remote caching (Vercel/Team City)
npx turbo login
npx turbo link

# CI/CD with cache
- run: pnpm install
- run: pnpm turbo run build --force=false

CI/CD Integration

GitHub Actions

yaml
# .github/workflows/ci.yml
name: CI

on: [push, pull_request]

jobs:
  build-and-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - uses: pnpm/action-setup@v2
        with:
          version: 8

      - uses: actions/setup-node@v3
        with:
          node-version: 18
          cache: 'pnpm'

      - name: Install dependencies
        run: pnpm install --frozen-lockfile

      - name: Run lint
        run: pnpm turbo run lint

      - name: Run typecheck
        run: pnpm turbo run typecheck

      - name: Run tests
        run: pnpm turbo run test

      - name: Build
        run: pnpm turbo run build

      - name: Cache Turborepo
        uses: actions/cache@v3
        with:
          path: .turbo
          key: ${{ runner.os }}-turbo-${{ github.sha }}
          restore-keys: |
            ${{ runner.os }}-turbo-

Nx CI

yaml
# .github/workflows/ci.yml
- uses: nrwl/nx-set-shas@v3
  with:
    main-branch-name: 'main'

- run: pnpm nx affected -t build test --parallel
  env:
    NX_BRANCH: ${{ github.event.pull_request.head.sha || github.sha }}
    NX_BASE: ${{ steps.set-shas.outputs.base }}

Versioning Strategy

Independent Versioning

bash
# Changesets for versioning
pnpm add -D @changesets/cli
pnpm changeset init

# Create changeset for changes
pnpm changeset

# Select packages and version bump
# @myorg/ui: minor
# @myorg/utils: patch

# Update versions
pnpm changeset version

# Publish packages
pnpm changeset publish

Fixed Versioning

bash
# Lerna for fixed versioning
pnpm add -D lerna

# lerna.json
{
  "version": "1.0.0",
  "packages": ["packages/*"],
  "npmClient": "pnpm"
}

# Bump all packages
pnpm lerna version patch

# Publish all packages
pnpm lerna publish from-git

Best Practices

1. Separate Apps from Libraries

bash
# apps/ - Deployable applications
# packages/ - Shared libraries

# Clear distinction between:
# - What users interact with (apps)
# - What developers use (packages)

2. Use Workspace Aliases

json
{
  "dependencies": {
    "@myorg/ui": "workspace:*"  // Always latest
  }
}

// For publishing, use:
"publishConfig": {
  "access": "public"
}

3. Optimize Build Performance

json
// turbo.json
{
  "pipeline": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**"],
      "inputs": ["src/**/*.tsx", "src/**/*.ts"]
    }
  }
}

// Only rebuild when inputs change
// Cache outputs

4. Enforce Code Standards

json
// .eslintrc.js
module.exports = {
  extends: ["@myorg/eslint-config"],
  ignorePatterns: ["node_modules/", "dist/"]
};

// Run across all packages
pnpm turbo run lint

Common Patterns

Microfrontend Monorepo

bash
my-monorepo/
├── apps/
│   ├── shell/              # Host application
│   ├── feature-auth/       # Auth feature
│   └── feature-dashboard/  # Dashboard feature
├── packages/
│   ├── shared-ui/          # Shared components
│   └── config/             # Module federation config

Full-Stack Monorepo

bash
my-monorepo/
├── apps/
│   ├── web/                # Next.js frontend
│   └── api/                # tRPC backend
├── packages/
│   ├── ui/                 # React components
│   ├── db/                 # Database client
│   └── types/              # Shared types

Design System Monorepo

bash
my-monorepo/
├── apps/
│   └── docs/               # Storybook docs
├── packages/
│   ├── components/         # React components
│   ├── icons/              # Icon library
│   └── theme/              # Theme configuration

Migration Strategy

Gradual Migration

bash
# Phase 1: Create monorepo
mkdir my-monorepo && cd my-monorepo
pnpm init

# Phase 2: Move one project
mkdir apps/web
# Copy existing project

# Phase 3: Extract shared code
mkdir packages/utils
# Move common utilities

# Phase 4: Add more projects
mkdir apps/api
# Move API project

Troubleshooting

Dependency Conflicts

bash
# Problem: Version conflicts
pnpm install
# error: peer dependency conflicts

# Solution: Use overrides
{
  "pnpm": {
    "overrides": {
      "react": "18.2.0"
    }
  }
}

Build Order Issues

bash
# Problem: Wrong build order
turbo run build

# Solution: Check dependencies
{
  "build": {
    "dependsOn": ["^build"]  // Build dependencies first
  }
}

Conclusion

Monorepos provide a powerful way to manage multiple related projects while maintaining code sharing and unified development practices. With modern tools like Turborepo, Nx, and pnpm workspaces, monorepos are more accessible than ever.

Key takeaways:

  • Use monorepos for related projects
  • Choose right tool (Nx/Turborepo/pnpm)
  • Optimize build with caching
  • Version packages independently
  • Integrate with CI/CD

Build better with monorepos!

Enjoyed this essay? Share it.

Related essays

Next essay
Polyrepos: When to Use Multiple Repositories