Monorepos offer significant benefits — shared code, atomic cross-package changes, unified tooling — but they come with complexity: build pipelines, caching, dependency graphs, circular dependencies, and coordinating changes across packages. Claude Code handles monorepo work well because it can navigate the entire workspace, understands package boundaries, and generates changes that stay consistent across packages.
This guide covers using Claude Code with monorepos: Turborepo pipelines, Nx configuration, shared packages, cross-package refactoring, and workspace dependency management.
Setting Up Claude Code for Monorepos
The workspace structure is the critical context:
# Monorepo Context
## Structure (Turborepo)
- apps/web — Next.js 15 app (customer-facing)
- apps/admin — Next.js 15 app (internal)
- apps/api — Node.js/Fastify API
- packages/ui — Shared React component library
- packages/config — Shared TS/ESLint/Tailwind configs
- packages/types — Shared TypeScript types
- packages/db — Drizzle schema + migrations (Postgres)
- packages/email — React Email templates
## Build Tool: Turborepo
- turbo.json defines pipeline: build depends on ^build (dep packages first)
- Package manager: pnpm with workspaces
- All packages use TypeScript — internal packages export from src/index.ts
## Conventions
- Shared types in packages/types — never duplicate in apps
- UI components in packages/ui — apps never define their own design system
- Each package has its own tsconfig.json extending packages/config/tsconfig.base.json
- Apps import packages as: @myco/ui, @myco/types, @myco/db
See the CLAUDE.md setup guide for complete configuration.
Turborepo Pipeline Configuration
Basic Pipeline
Configure Turborepo for a monorepo with Next.js apps and shared packages.
Build should respect dependency order: packages build before apps.
Lint and type-check can run in parallel.
{
"$schema": "https://turbo.build/schema.json",
"globalEnv": ["NODE_ENV", "DATABASE_URL"],
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": [".next/**", "dist/**", "!.next/cache/**"],
"env": ["NEXT_PUBLIC_API_URL", "NEXT_PUBLIC_STRIPE_KEY"]
},
"test": {
"dependsOn": ["^build"],
"outputs": ["coverage/**"]
},
"lint": {
"outputs": []
},
"type-check": {
"dependsOn": ["^build"],
"outputs": []
},
"dev": {
"cache": false,
"persistent": true
}
}
}
"dependsOn": ["^build"] is the key: the ^ means “build all packages this package depends on first.” Apps depend on @myco/ui and @myco/types, so those build first. Parallel packages build concurrently.
Caching Strategy
Our CI pipeline is slow because the build cache isn't hitting.
The turbo remote cache is configured but cache misses on every PR.
Claude diagnoses common cache invalidation issues:
outputspatterns not matching actual build outputs (.next/**missing some directories)- Missing
enventries that affect the build (a new env var added but not declared inturbo.json) globalEnvchanges invalidating all cachespnpm-lock.yamlchanges causing package rebuilds
It reads your turbo.json alongside the CI logs and identifies the specific mismatch.
Cross-App Commands
Run build for only the web app and its dependency chain:
# Turbo automatically builds only what web app depends on
turbo build --filter=@myco/web
# Build all affected by changes to packages/ui:
turbo build --filter=...[packages/ui]
# Run tests only for packages changed since main:
turbo test --filter=[main]
Claude knows Turborepo’s filter syntax — [main] for changed packages, ... for dependents, ^ for dependencies.
Nx Configuration
For Nx workspaces:
Configure Nx for a workspace with Angular apps and shared libraries.
Set up the project graph correctly for: affected builds,
caching, and parallelism.
{
"tasksRunnerOptions": {
"default": {
"runner": "nx/tasks-runners/default",
"options": {
"cacheableOperations": ["build", "test", "lint", "e2e"],
"parallel": 4
}
}
},
"targetDefaults": {
"build": {
"dependsOn": ["^build"],
"cache": true
},
"test": {
"dependsOn": ["build"],
"cache": true
}
},
"defaultProject": "web"
}
Claude generates project.json files for each project with the correct targets and tags for the project graph. Nx’s affected commands use the graph to determine what changed.
Shared Packages
Creating a Shared Types Package
Create a shared types package that both apps and the API can import.
It should export: User, Order, ApiResponse<T>, and all enum types.
packages/types/
├── package.json
├── tsconfig.json
└── src/
├── index.ts
├── user.ts
├── order.ts
└── api.ts
// packages/types/src/user.ts
export interface User {
id: string;
email: string;
name: string;
role: UserRole;
createdAt: string; // ISO 8601 — serialization-safe across apps
}
export type UserRole = 'admin' | 'member' | 'viewer';
// packages/types/src/api.ts
export type ApiResponse<T> =
| { success: true; data: T }
| { success: false; error: { code: string; message: string } };
// packages/types/src/index.ts
export * from './user';
export * from './order';
export * from './api';
// packages/types/package.json
{
"name": "@myco/types",
"version": "0.0.1",
"main": "./src/index.ts",
"types": "./src/index.ts",
"exports": {
".": "./src/index.ts"
}
}
The "main": "./src/index.ts" pattern — pointing directly to TypeScript source — works in monorepos because TypeScript resolves it correctly within the workspace. No build step needed for type-only packages.
Shared UI Component Library
Add a Button component to packages/ui.
It should use Tailwind and shadcn conventions.
Both apps import it as @myco/ui.
Claude generates the component in packages/ui/src/button.tsx, updates packages/ui/src/index.ts to export it, and checks that the tailwind.config.js in the consuming apps includes the packages/ui/src/** glob in content (required for Tailwind to include the classes from external packages).
// In apps, after adding:
import { Button } from '@myco/ui';
Claude also handles the TypeScript path mapping if the workspace uses path aliases — ensuring @myco/ui resolves to the package source rather than an undefined module.
Cross-Package Refactoring
This is where Claude Code’s full-codebase access is most valuable — changes that span multiple packages:
Rename UserRole to MemberRole across the entire monorepo.
It's defined in packages/types and used in apps/web, apps/admin, and apps/api.
Claude finds all usages:
# Claude reads all files rather than guessing
grep -r "UserRole" apps/ packages/ --include="*.ts" --include="*.tsx"
Then systematically updates:
packages/types/src/user.ts— rename the typeapps/web/**— all imports and usagesapps/admin/**— all imports and usagesapps/api/**— all imports and usages
This cross-package rename is exactly the kind of refactoring where monorepos shine — one atomic change, one PR. Claude Code handles it in one pass rather than requiring you to manually grep and update each package.
Adding a Field Across Packages
Add a timezone field to the User type.
It needs to: appear in the types package, be in the DB schema,
be returned by the API, and be editable in the web app profile page.
Claude generates the full change:
packages/types/src/user.ts— addtimezone: stringpackages/db/schema.ts— addtimezonecolumnpackages/db/migrations/— generate migration SQLapps/api/src/users/— include in response, handle in PUT endpointapps/web/src/profile/— add timezone selector to the profile form
This is the kind of change that’s tedious to describe in a ticket but fast to implement when Claude sees the whole workspace.
Dependency Management
Avoiding Circular Dependencies
My build is failing with a circular dependency.
packages/db imports from packages/types,
but I also want packages/types to import DB-derived types.
How do I fix the circular dependency?
Claude identifies the pattern and proposes a solution: extract the shared primitives into a new packages/core package that neither db nor types depends on, then have both import from packages/core. It maps the dependency graph and generates the restructured packages.
Keeping Versions Synchronized
I'm getting "multiple versions of React" errors.
Some packages installed React 18, others have React 19.
Claude reads pnpm-lock.yaml to identify which packages have divergent React versions, then generates the peerDependencies + .npmrc shamefully-hoist configuration needed to de-duplicate, and suggests when to use overrides in the root package.json.
CI/CD for Monorepos
Set up a GitHub Actions workflow that only builds and tests
packages affected by the PR changes.
Use Turborepo remote caching.
- name: Build affected
run: pnpm turbo build --filter=[origin/main]
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
- name: Test affected
run: pnpm turbo test --filter=[origin/main]
env:
DATABASE_URL: ${{ secrets.TEST_DATABASE_URL }}
--filter=[origin/main] tells Turborepo to only run tasks for packages with changes since the main branch. Remote caching (via Vercel or Turborepo Cloud) serves cached artifacts from prior runs — reducing CI time from 10 minutes to under 2 for unchanged packages.
Working with Monorepos and Claude Code
The main challenge in monorepos is navigating the workspace — understanding which package owns which concern, where to add a new type vs. a new component, and tracking the dependency graph. Claude Code’s strength here is reading the workspace as a whole rather than one file at a time.
Give Claude the full context: your turbo.json or nx.json, the package list, and the workspace conventions from CLAUDE.md. With that context, cross-package changes become straightforward — ask for a feature and get the full set of changes across all affected packages.
For the deployment side of monorepos, see the Docker guide for containerizing individual apps from a monorepo context and the CI/CD guide for GitHub Actions integration. The TypeScript guide covers the advanced TypeScript patterns used in shared packages. The Claude Skills 360 bundle includes monorepo skill sets for Turborepo and Nx setups. Start with the free tier.