Skip to main content
{/}codefunc
GuidesJuly 7, 2026 · 6 min read

Monorepos for Frontend: Turborepo, Nx, and Workspace Management

A practical guide to monorepo architecture — when to use monorepos, setting up Turborepo or Nx, shared packages, caching, and CI/CD optimization.

By codefunc

monorepoturboreponxfrontendci-cdworkspace

Monorepos consolidate multiple projects into a single repository. Instead of separate repos for your web app, mobile app, shared components, and utilities, everything lives together. This simplifies code sharing, atomic changes across projects, and consistent tooling. The tradeoff is complexity in build systems and CI.

Bottom line: Use a monorepo when you have multiple related packages that share code. Use Turborepo for simplicity and speed. Use Nx for larger teams needing advanced features. Configure caching and affected-only builds from day one. Monorepos shine for design systems, shared utilities, and apps with common business logic.

When to use a monorepo

Good fit

  • Multiple apps sharing components/utilities
  • Design system with consuming apps
  • Full-stack TypeScript (frontend + backend)
  • Microservices with shared contracts
  • Team working across multiple related projects

Poor fit

  • Single application with no shared code
  • Completely unrelated projects
  • Teams with strict code isolation requirements
  • Very large codebases without proper tooling

Monorepo benefits

Benefit Description
Code sharing Import shared packages directly
Atomic changes Update library and consumers in one commit
Consistent tooling Same lint, test, build config everywhere
Simplified dependencies Single lock file, no version mismatches
Better refactoring IDE understands entire codebase

Monorepo challenges

Challenge Solution
Slow builds Caching, affected-only builds
Large repo size Sparse checkout, shallow clone
CI complexity Task orchestration (Turborepo/Nx)
Ownership boundaries CODEOWNERS, project constraints

Turborepo

Turborepo is a fast, simple monorepo build system.

Setup

npx create-turbo@latest my-monorepo
cd my-monorepo

Structure

my-monorepo/
├── apps/
│   ├── web/              # Next.js app
│   │   └── package.json
│   └── docs/             # Documentation site
│       └── package.json
├── packages/
│   ├── ui/               # Shared components
│   │   └── package.json
│   ├── utils/            # Shared utilities
│   │   └── package.json
│   └── config/           # Shared configs (ESLint, TS)
│       └── package.json
├── package.json          # Root package.json
├── turbo.json            # Turborepo config
└── pnpm-workspace.yaml   # Workspace definition

Root package.json

{
  "name": "my-monorepo",
  "private": true,
  "scripts": {
    "build": "turbo build",
    "dev": "turbo dev",
    "lint": "turbo lint",
    "test": "turbo test"
  },
  "devDependencies": {
    "turbo": "^2.0.0"
  },
  "packageManager": "pnpm@9.0.0"
}

turbo.json

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

pnpm-workspace.yaml

packages:
  - "apps/*"
  - "packages/*"

Package dependencies

// apps/web/package.json
{
  "name": "web",
  "dependencies": {
    "@repo/ui": "workspace:*",
    "@repo/utils": "workspace:*"
  }
}
// packages/ui/package.json
{
  "name": "@repo/ui",
  "main": "./src/index.ts",
  "exports": {
    ".": "./src/index.ts"
  }
}

Using shared packages

// apps/web/app/page.tsx
import { Button } from '@repo/ui';
import { formatDate } from '@repo/utils';

export default function Home() {
  return (
    <div>
      <Button>Click me</Button>
      <p>{formatDate(new Date())}</p>
    </div>
  );
}

Remote caching

# Login to Vercel for remote cache
npx turbo login

# Link repo
npx turbo link

# Now builds are cached remotely
npx turbo build

Nx

Nx is a more feature-rich monorepo tool with advanced capabilities.

Setup

npx create-nx-workspace@latest my-nx-monorepo

Structure

my-nx-monorepo/
├── apps/
│   ├── web/
│   └── web-e2e/
├── libs/
│   ├── ui/
│   └── utils/
├── nx.json
├── package.json
└── tsconfig.base.json

nx.json

{
  "targetDefaults": {
    "build": {
      "dependsOn": ["^build"],
      "cache": true
    },
    "test": {
      "cache": true
    },
    "lint": {
      "cache": true
    }
  },
  "defaultBase": "main"
}

Generators

Nx has generators for scaffolding:

# Generate a new React library
nx g @nx/react:library ui --directory=libs/ui

# Generate a new Next.js app
nx g @nx/next:application web --directory=apps/web

# Generate a component
nx g @nx/react:component Button --project=ui

Running tasks

# Run single target
nx build web

# Run for all projects
nx run-many -t build

# Run only affected projects
nx affected -t build

# Visualize dependency graph
nx graph

Project boundaries

Enforce import rules with tags:

// libs/ui/project.json
{
  "tags": ["scope:shared", "type:ui"]
}
// .eslintrc.json
{
  "rules": {
    "@nx/enforce-module-boundaries": [
      "error",
      {
        "depConstraints": [
          {
            "sourceTag": "type:app",
            "onlyDependOnLibsWithTags": ["type:ui", "type:util"]
          },
          {
            "sourceTag": "type:ui",
            "onlyDependOnLibsWithTags": ["type:util"]
          }
        ]
      }
    ]
  }
}

Shared configurations

ESLint config package

// packages/config-eslint/package.json
{
  "name": "@repo/eslint-config",
  "main": "index.js"
}
// packages/config-eslint/index.js
module.exports = {
  extends: ['next', 'turbo', 'prettier'],
  rules: {
    '@next/next/no-html-link-for-pages': 'off',
  },
};
// apps/web/.eslintrc.js
module.exports = {
  root: true,
  extends: ['@repo/eslint-config'],
};

TypeScript config package

// packages/config-typescript/base.json
{
  "$schema": "https://json.schemastore.org/tsconfig",
  "compilerOptions": {
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "moduleResolution": "bundler",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true
  }
}
// apps/web/tsconfig.json
{
  "extends": "@repo/typescript-config/nextjs.json",
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"]
    }
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
  "exclude": ["node_modules"]
}

CI/CD optimization

Affected-only builds

# .github/workflows/ci.yml
name: CI
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0  # Need full history for affected

      - uses: pnpm/action-setup@v3
        with:
          version: 9

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'pnpm'

      - run: pnpm install

      # Turborepo
      - run: pnpm turbo build --filter=...[origin/main]

      # Or Nx
      - run: npx nx affected -t build --base=origin/main

Caching in CI

# Turborepo remote caching
- run: pnpm turbo build
  env:
    TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
    TURBO_TEAM: ${{ vars.TURBO_TEAM }}

Parallel jobs by project

jobs:
  detect:
    runs-on: ubuntu-latest
    outputs:
      projects: ${{ steps.affected.outputs.projects }}
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - id: affected
        run: |
          PROJECTS=$(npx nx show projects --affected --base=origin/main --json)
          echo "projects=$PROJECTS" >> $GITHUB_OUTPUT

  build:
    needs: detect
    runs-on: ubuntu-latest
    strategy:
      matrix:
        project: ${{ fromJson(needs.detect.outputs.projects) }}
    steps:
      - run: npx nx build ${{ matrix.project }}

Common patterns

Internal packages (no build step)

// packages/ui/package.json
{
  "name": "@repo/ui",
  "main": "./src/index.ts",
  "types": "./src/index.ts",
  "exports": {
    ".": "./src/index.ts",
    "./button": "./src/button.tsx"
  }
}

Consuming apps bundle directly from source.

Publishable packages

// packages/ui/package.json
{
  "name": "@repo/ui",
  "main": "./dist/index.js",
  "types": "./dist/index.d.ts",
  "scripts": {
    "build": "tsup src/index.ts --format cjs,esm --dts"
  }
}

Versioning

For internal packages:

"@repo/ui": "workspace:*"

For publishable packages, use changesets:

npx changeset init
npx changeset  # Create changeset
npx changeset version  # Update versions
npx changeset publish  # Publish to npm

Troubleshooting

"Package not found" errors

  1. Check workspace definition includes package path
  2. Run pnpm install after adding packages
  3. Verify package.json name matches import

Slow builds

  1. Enable caching in turbo.json/nx.json
  2. Use remote caching (Turborepo/Nx Cloud)
  3. Run affected-only in CI
  4. Optimize outputs in task config

Type errors in shared packages

  1. Check tsconfig.json references
  2. Ensure exports field is correct
  3. Restart TypeScript server after changes

Use Text Diff to compare configurations between packages. Format package.json files with JSON Formatter. Generate gitignore for monorepos with Gitignore Generator.

Practical takeaway

Monorepos work well for related projects sharing code. Start with Turborepo for simplicity — it handles caching and task orchestration with minimal config. Graduate to Nx if you need generators, project constraints, or advanced affected analysis. Configure remote caching from day one. Use affected-only builds in CI to keep pipelines fast. Internal packages without a build step are simplest for sharing components.

Sources

Try it on codefunc

Related articles

3

Search tools

Find a developer tool