Monorepo patterns with Turborepo and pnpm workspaces — shared packages, pipeline config, caching.
A monorepo is not "all code in one folder." It's a dependency graph with clear boundaries. If your packages depend on each other in unpredictable ways, you don't have a monorepo — you have a monolith.
root/
├── apps/
│ ├── web/ # Next.js frontend
│ ├── api/ # Express/Fastify backend
│ └── mobile/ # React Native
├── packages/
│ ├── ui/ # Shared component library
│ ├── config/ # Shared tsconfig, eslint, prettier
│ ├── utils/ # Shared utility functions
│ └── types/ # Shared TypeScript types
├── turbo.json
├── pnpm-workspace.yaml
└── package.json
apps/ — deployable applications. Each has its own build, deploy, and runtime.packages/ — shared libraries consumed by apps. Not deployed independently.pnpm-workspace.yaml:
packages:
- "apps/*"
- "packages/*"
workspace:* for versioning:
{
"dependencies": {
"@repo/ui": "workspace:*",
"@repo/utils": "workspace:*"
}
}
workspace:* resolves to the local package. pnpm replaces it with the actual version on publish.package.json: TypeScript, ESLint, Prettier, Turbo.package.json: Next.js in apps/web, Express in apps/api.react is in root, don't add it to each app.pnpm install from root. Always.pnpm --filter <package> add <dep> to add deps to specific packages.exports in package.json:
{
"name": "@repo/ui",
"exports": {
".": "./src/index.ts",
"./button": "./src/button.tsx",
"./card": "./src/card.tsx"
}
}
tsconfig.base.json in packages/config/:
{
"compilerOptions": {
"strict": true,
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"paths": { "@repo/*": ["../../packages/*/src"] }
}
}
{ "extends": "@repo/config/tsconfig.base.json" }
turbo.json defines the build pipeline:
{
"$schema": "https://turbo.build/schema.json",
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", ".next/**"]
},
"dev": {
"cache": false,
"persistent": true
},
"lint": {
"dependsOn": ["^build"]
},
"test": {
"dependsOn": ["^build"]
}
}
}
^build means "build my dependencies first." This ensures packages are built before apps that consume them.outputs tells Turbo what to cache. Be explicit.npx turbo login
npx turbo link
pnpm turbo build — build everything in dependency order.pnpm turbo lint --filter=apps/web — target specific packages with --filter.turbo dev runs all dev scripts in parallel with proper dependency ordering.tsconfig.json extending the shared base. Path aliases must be configured in both tsconfig AND the bundler.outputs in turbo.json — Turbo can't cache without it.utils package — split by domain: @repo/date-utils, @repo/string-utils.