Monorepo Patterns
Purpose
Guide architecture decisions for monorepo setups using modern tooling. Covers Turborepo, pnpm workspaces, Nx, shared packages, build caching, dependency management, and scaling strategies.
Activation
Use this skill when the user asks about:
- Setting up a monorepo
- Configuring workspaces (pnpm, npm, yarn)
- Turborepo or Nx configuration
- Sharing code between packages
- Build caching and optimization
- Dependency management across packages
- CI/CD for monorepos
Project Structure
Recommended Layout
monorepo/
├── apps/
│ ├── web/ # Next.js frontend
│ │ ├── package.json
│ │ ├── tsconfig.json
│ │ └── src/
│ ├── api/ # Express/Fastify backend
│ │ ├── package.json
│ │ ├── tsconfig.json
│ │ └── src/
│ └── docs/ # Documentation site
│ ├── package.json
│ └── src/
├── packages/
│ ├── ui/ # Shared React components
│ │ ├── package.json
│ │ ├── tsconfig.json
│ │ └── src/
│ ├── config-eslint/ # Shared ESLint config
│ │ ├── package.json
│ │ └── index.js
│ ├── config-typescript/ # Shared TSConfig
│ │ ├── package.json
│ │ ├── base.json
│ │ ├── nextjs.json
│ │ └── node.json
│ ├── shared/ # Shared utilities and types
│ │ ├── package.json
│ │ ├── tsconfig.json
│ │ └── src/
│ └── database/ # Prisma schema and client
│ ├── package.json
│ ├── prisma/
│ └── src/
├── tooling/
│ ├── scripts/ # Shared build/deploy scripts
│ └── docker/ # Shared Dockerfiles
├── turbo.json
├── pnpm-workspace.yaml
├── package.json
├── .npmrc
└── tsconfig.json
pnpm Workspaces
pnpm-workspace.yaml
packages:
- "apps/*"
- "packages/*"
- "tooling/*"
Root package.json
{
"name": "monorepo",
"private": true,
"scripts": {
"build": "turbo build",
"dev": "turbo dev",
"lint": "turbo lint",
"test": "turbo test",
"typecheck": "turbo typecheck",
"clean": "turbo clean",
"format": "prettier --write \"**/*.{ts,tsx,js,jsx,md,json}\""
},
"devDependencies": {
"prettier": "^3.0.0",
"turbo": "^2.0.0"
},
"packageManager": "pnpm@9.0.0",
"engines": {
"node": ">=20.0.0"
}
}
.npmrc
auto-install-peers=true
strict-peer-dependencies=false
link-workspace-packages=true
Internal Package References
In a consuming package's package.json:
{
"dependencies": {
"@repo/ui": "workspace:*",
"@repo/shared": "workspace:*",
"@repo/database": "workspace:*"
}
}
Turborepo Configuration
turbo.json
{
"$schema": "https://turbo.build/schema.json",
"globalDependencies": [
"**/.env.*local",
"**/.env"
],
"globalEnv": [
"NODE_ENV",
"CI"
],
"tasks": {
"build": {
"dependsOn": ["^build"],
"inputs": ["src/**", "tsconfig.json", "package.json"],
"outputs": ["dist/**", ".next/**", "!.next/cache/**"],
"env": ["DATABASE_URL", "NEXT_PUBLIC_*"]
},
"dev": {
"dependsOn": ["^build"],
"cache": false,
"persistent": true
},
"lint": {
"dependsOn": ["^build"],
"inputs": ["src/**", ".eslintrc*", "eslint.config.*"]
},
"typecheck": {
"dependsOn": ["^build"],
"inputs": ["src/**", "tsconfig.json"]
},
"test": {
"dependsOn": ["^build"],
"inputs": ["src/**", "tests/**", "vitest.config.*"],
"outputs": ["coverage/**"]
},
"clean": {
"cache": false
}
}
}
Key Concepts
^buildmeans "run build in all dependencies first" (topological ordering)inputsdefine what files affect the cache hashoutputsdefine what gets cachedpersistentmarks long-running tasks like dev serverscache: falsedisables caching for tasks with side effects
Filtering
# Run only for a specific package
turbo build --filter=@repo/web
# Run for a package and its dependencies
turbo build --filter=@repo/web...
# Run for packages that changed since main
turbo build --filter=[main]
# Run for packages in apps/ directory
turbo build --filter=./apps/*
# Combine filters
turbo build --filter=@repo/web... --filter=!@repo/docs
Shared Package Patterns
Shared TypeScript Config
packages/config-typescript/base.json:
{
"$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": {
"strict": true,
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"isolatedModules": true,
"incremental": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true
},
"exclude": ["node_modules", "dist"]
}
packages/config-typescript/nextjs.json:
{
"extends": "./base.json",
"compilerOptions": {
"lib": ["dom", "dom.iterable", "ES2022"],
"module": "ESNext",
"jsx": "preserve",
"plugins": [{ "name": "next" }],
"noEmit": true
}
}
Shared UI Package
packages/ui/package.json:
{
"name": "@repo/ui",
"version": "0.0.0",
"private": true,
"type": "module",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
},
"./button": {
"types": "./dist/components/button.d.ts",
"import": "./dist/components/button.js"
}
},
"scripts": {
"build": "tsup",
"dev": "tsup --watch",
"typecheck": "tsc --noEmit"
},
"devDependencies": {
"@repo/config-typescript": "workspace:*",
"tsup": "^8.0.0",
"typescript": "^5.5.0"
}
}
packages/ui/tsup.config.ts:
import { defineConfig } from "tsup";
export default defineConfig({
entry: ["src/index.ts", "src/components/*.tsx"],
format: ["esm"],
dts: true,
splitting: true,
clean: true,
external: ["react", "react-dom"],
});
Shared Utilities Package
packages/shared/package.json:
{
"name": "@repo/shared",
"version": "0.0.0",
"private": true,
"type": "module",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
},
"./types": {
"types": "./dist/types/index.d.ts",
"import": "./dist/types/index.js"
},
"./utils": {
"types": "./dist/utils/index.d.ts",
"import": "./dist/utils/index.js"
}
},
"scripts": {
"build": "tsup",
"dev": "tsup --watch"
}
}
Build Caching
Remote Caching with Vercel
# Login to Vercel remote cache
npx turbo login
npx turbo link
Or via environment variables in CI:
TURBO_TOKEN=your_token
TURBO_TEAM=your_team
Custom Remote Cache
In turbo.json:
{
"remoteCache": {
"enabled": true,
"signature": true
}
}
Cache Debugging
# Check what turbo will run (dry run)
turbo build --dry-run
# Show verbose output for cache hits/misses
turbo build --verbosity=2
# Force ignore cache
turbo build --force
# Show task graph
turbo build --graph
Dependency Management
Version Alignment
Use pnpm catalog feature or syncpack to keep versions aligned:
pnpm-workspace.yaml with catalog:
packages:
- "apps/*"
- "packages/*"
catalog:
react: "^18.3.0"
react-dom: "^18.3.0"
typescript: "^5.5.0"
vitest: "^2.0.0"
zod: "^3.23.0"
Then in any package.json:
{
"dependencies": {
"react": "catalog:",
"react-dom": "catalog:"
}
}
Dependency Rules
- Shared dependencies go in root only if they are dev tools (prettier, turbo)
- Each package declares its own dependencies - never rely on hoisting
- Internal packages use
workspace:*for linking - Pin exact versions for critical dependencies in production apps
- Use
peerDependenciesfor shared libraries that consumers must provide (React, etc.)
Scaling Patterns
When the Monorepo Grows
| Scale | Strategy |
|---|---|
| < 10 packages | Standard Turborepo setup |
| 10-50 packages | Add remote caching, strict task inputs |
| 50-200 packages | Consider Nx for fine-grained caching |
| 200+ packages | Custom tooling, Bazel, or split into multiple repos |
Code Ownership
Use CODEOWNERS to assign package owners:
# .github/CODEOWNERS
/apps/web/ @frontend-team
/apps/api/ @backend-team
/packages/ui/ @design-system-team
/packages/shared/ @platform-team
/packages/database/ @backend-team
Common Mistakes
| Mistake | Impact | Fix |
|---|---|---|
No inputs in turbo.json |
Caching is ineffective | Define precise inputs per task |
Importing from src/ of other packages |
Fragile, no build step | Use exports field and built output |
Shared node_modules assumptions |
Works locally, fails in CI | Each package declares its deps |
No .npmrc config |
Phantom dependencies | Set strict-peer-dependencies |
| Giant shared package | Everything depends on everything | Split into focused packages |
| No typecheck task | Type errors caught late | Add typecheck to pipeline |