Monorepo management with Turborepo and pnpm workspaces. Use for Turborepo setup, turbo.json task dependencies, remote caching, pnpm workspace protocol, shared packages (ui-library, config, types), affected-only CI/CD builds, or monorepo structure (apps/ vs packages/). Covers pitfalls like circular deps, version drift, and hoisting issues.
Install
npx skillscat add saajunaid/caddis-plugin/monorepo Install via the SkillsCat registry.
Monorepo Skill
Turborepo + pnpm workspaces for managing multi-app, multi-package repositories efficiently.
1. Repository Structure
monorepo/
├── apps/
│ ├── web/ # Next.js app
│ ├── api/ # FastAPI or Express backend
│ └── docs/ # Docusaurus or similar
├── packages/
│ ├── ui/ # Shared React component library
│ ├── config/ # Shared ESLint, TypeScript, Tailwind configs
│ ├── types/ # Shared TypeScript types / Zod schemas
│ └── utils/ # Shared utility functions
├── turbo.json
├── pnpm-workspace.yaml
└── package.json # Root — scripts only, no app deps2. pnpm Workspaces
# pnpm-workspace.yaml
packages:
- 'apps/*'
- 'packages/*'// Root package.json
{
"name": "my-monorepo",
"private": true,
"scripts": {
"dev": "turbo dev",
"build": "turbo build",
"test": "turbo test",
"lint": "turbo lint",
"typecheck": "turbo typecheck",
"clean": "turbo clean && rm -rf node_modules"
},
"devDependencies": {
"turbo": "^2.0.0"
},
"engines": { "node": ">=20", "pnpm": ">=9" }
}workspace: Protocol — Cross-Package Dependencies
// apps/web/package.json
{
"dependencies": {
"@myapp/ui": "workspace:*", // Always latest local version
"@myapp/types": "workspace:^", // Semver compatible local version
"@myapp/utils": "workspace:*"
}
}Rules:
workspace:*— consume local package always (recommended for internal packages)workspace:^— enforces semver compatibility (use for packages that may be published)- Never use relative paths (
"../packages/ui") — use workspace protocol
3. Turborepo Configuration
// turbo.json
{
"$schema": "https://turbo.build/schema.json",
"ui": "tui",
"tasks": {
"build": {
"dependsOn": ["^build"], // Build dependencies first (topological)
"inputs": ["src/**", "package.json"],
"outputs": [".next/**", "dist/**", "!.next/cache/**"]
},
"dev": {
"persistent": true, // Keep running (dev servers)
"cache": false, // Never cache watch mode
"dependsOn": ["^build"]
},
"test": {
"dependsOn": ["^build"],
"inputs": ["src/**", "test/**", "vitest.config.ts"],
"outputs": ["coverage/**"]
},
"lint": {
"inputs": ["src/**", ".eslintrc*", "eslint.config.*"]
},
"typecheck": {
"dependsOn": ["^build"], // Types may be generated by build
"inputs": ["src/**", "tsconfig*.json"]
},
"clean": {
"cache": false
}
}
}Task Dependency Conventions
"dependsOn": ["^build"]— wait for all dependencies'buildtasks to finish first"dependsOn": ["build"]— wait for the same package'sbuildtask first"cache": false— never cache this task (use for watch/serve tasks)"persistent": true— marks tasks that run indefinitely (dev servers), prevents pipeline hanging
4. Remote Caching
# Login to Vercel Remote Cache (free)
npx turbo login
npx turbo link # Links to your team's remote cache
# CI: set environment variable
TURBO_TOKEN=<token>
TURBO_TEAM=<team-slug># GitHub Actions with remote cache
- name: Build
run: turbo build
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ vars.TURBO_TEAM }}Cache hit = unchanged output = skip the task entirely. A 100% cache hit monorepo build in CI can drop from 10 minutes to seconds.
5. Shared Packages
packages/config — Shared TypeScript/ESLint/Tailwind Config
// packages/config/package.json
{
"name": "@myapp/config",
"version": "0.0.0",
"private": true,
"exports": {
"./typescript": "./typescript/base.json",
"./eslint/base": "./eslint/base.js",
"./tailwind": "./tailwind/index.ts"
}
}// apps/web/tsconfig.json
{
"extends": "@myapp/config/typescript",
"compilerOptions": { "outDir": "dist" },
"include": ["src", "next-env.d.ts"]
}packages/ui — Shared Component Library
// packages/ui/package.json
{
"name": "@myapp/ui",
"exports": {
".": {
"import": "./src/index.ts", // For bundlers (Vite/Next.js/Turbopack)
"types": "./src/index.ts"
}
},
"peerDependencies": {
"react": ">=18",
"react-dom": ">=18"
}
}Pattern: Export from src/index.ts directly — let the consuming app's bundler handle transpilation. Avoids a double-build for internal packages.
packages/types — Shared Zod Schemas and Types
// packages/types/src/index.ts
export * from './user'
export * from './product'
export * from './common'Both apps and API share these schemas — single source of truth for request/response contracts.
6. CI/CD — Affected-Only Builds
# .github/workflows/ci.yml
name: CI
context: fork
on: [push, pull_request]
jobs:
build-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with: { fetch-depth: 2 } # Need previous commit for --filter
- uses: pnpm/action-setup@v4
with: { version: 9 }
- uses: actions/setup-node@v4
with: { node-version: 20, cache: 'pnpm' }
- run: pnpm install --frozen-lockfile
- name: Build and Test (affected only)
run: turbo run build test lint --filter="...[HEAD^1]"
# --filter="...[HEAD^1]" = only packages changed since last commit
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ vars.TURBO_TEAM }}Useful Filter Patterns
# Only changed packages and their dependents
turbo build --filter="...[HEAD^1]"
# Only a specific app and its deps
turbo build --filter="web..."
# All packages except one
turbo build --filter="!docs"
# Specific package
turbo test --filter="@myapp/ui"7. Pitfalls
| Pitfall | Symptom | Fix |
|---|---|---|
| Circular dependencies | Build never completes (cycle in task graph) | madge --circular --extensions ts src/ to detect; extract shared code |
| Version drift | @myapp/config at v1 in one app, v2 in another |
workspace:* for all internal packages |
| Hoisting issues | Package works locally, fails in CI | Use pnpm.overrides or .npmrc public-hoist-pattern |
Missing ^build in dev |
Dev server crashes because shared types not compiled | Add "dependsOn": ["^build"] to dev task |
| Cache poisoning | Stale build output served | Add all relevant source files to inputs |
persistent: true missing on dev |
turbo dev exits immediately |
Always mark watch-mode tasks as persistent: true |
Cross-package imports via ../../ |
Breaks when packages move | Always use workspace:* — never relative paths |
8. Checklist
-
pnpm-workspace.yamlcoversapps/*andpackages/* - All internal packages use
workspace:*independencies -
turbo.jsonhas^buildindependsOnfor transitive tasks -
devtask haspersistent: trueandcache: false - Remote cache configured for CI (Vercel or self-hosted)
-
--filter="...[HEAD^1]"used in CI for affected-only builds - Shared TypeScript config extends from
@myapp/config/typescript -
packages/typesholds all cross-boundary Zod schemas - No circular imports between packages (run
madgecheck) - No relative path cross-package imports (
../../packages/)