Monorepo
Project-specific patterns for pnpm workspaces + Turborepo.
Architecture Decisions
Workspace Organization
- Split apps from packages —
apps/for deployables,packages/for shared libraries. - Namespace packages — Prefix with
@org/to avoid npm conflicts. - Single lockfile —
pnpm-lock.yamlat root only. Never commit multiple lockfiles. - No cross-package file access — Never use
../to reach into other packages; import via dependencies.
Dependency Management
- Use
workspace:*protocol — Always for internal package dependencies. - Hoist common devDependencies — Shared tooling (TypeScript, ESLint) in root.
- Peer dependencies for frameworks — React, Vue, etc. as peers to avoid version conflicts.
- Consider Catalogs (pnpm 9.5+) — Centralize versions in
pnpm-workspace.yamlfor large repos.
Turborepo Tasks
- Use
^for build dependencies —"dependsOn": ["^build"]for topological order. - Always define
outputs— Without outputs, nothing gets cached. - Mark dev servers as persistent —
"persistent": true, "cache": false. - Be explicit about environment — List all build-affecting vars in
envorglobalEnv.
Gotchas
- Missing
outputsin turbo.json silently disables caching for that task. The task runs every time and you won't get an error — just slow builds. Always verify outputs are configured. pnpm installdoes NOT respect--filterfor installation — it always installs the entire workspace. Filtering only works forpnpm runandpnpm exec.workspace:*resolves to the CURRENT version of the local package, not "latest from npm". If the package has"version": "0.0.0", published packages will have"dependency": "0.0.0"— set meaningful versions before publishing.- Turborepo's
envfield in turbo.json uses GLOB patterns, not exact matches."env": ["API_*"]capturesAPI_KEY,API_URL, etc. Forgetting this causes over-invalidation. turbo run build --filter=app-abuilds app-a AND all its workspace dependencies. If a dependency fails, app-a won't build. Check transitive deps.- Adding a package to
packages/requires runningpnpm installbefore the workspace recognizes it. The new package also needs a validpackage.jsonwithnamematching the workspace pattern. - TypeScript project references (
referencesin tsconfig.json) must match the workspace dependency graph. Mismatches cause type errors that only appear duringtsc --build, not in IDE. turbo.json'sglobalDependenciesinvalidates ALL tasks when listed files change. Don't put frequently-changed files here — use task-levelinputsinstead.- Shared Tailwind configs need
@sourcedirectives pointing to consuming packages' source directories, otherwise classes used in shared packages are purged. pnpm deploy(for production) copies a single package and its dependencies to a target directory. It does NOT run build scripts — build first, then deploy.- Remote cache (Vercel or self-hosted) requires
outputsto be correct. If outputs are wrong, cached artifacts will be incomplete and downstream tasks break silently. persistent: truetasks preventturbo runfrom exiting. Don't include persistent tasks in CI pipelines unless they have a timeout.
References
| When you need... | Read |
|---|---|
| workspace.yaml, workspace: protocol, filtering | pnpm-workspace.md |
| turbo.json schema, tasks, dependsOn | turborepo.md |
| Cache outputs/inputs, remote cache setup | caching.md |
| Directory layout, package naming, tsconfig | structure.md |
| Tailwind v4 shared theme package | tailwind-v4.md |