- Published on
Turborepo Monorepo — Scaling to 100+ Packages With Shared Config and Fast Builds
- Authors

- Name
- Sanjeev Sharma
- @webcoderspeed1
Introduction
Polyrepos—separate repositories for each package—don't scale. You can't share code without npm publishing, you can't refactor cross-package without coordinating PRs, you can't test changes atomically.
Monorepos solve this. Turborepo makes monorepos fast: intelligent task scheduling, remote caching, and dependency graph pruning ensure you rebuild only what changed.
This guide builds a production monorepo scaling from 10 to 100+ packages.
- Workspace Setup With npm/pnpm Workspaces
- turbo.json Pipeline Configuration
- Remote Caching With Vercel
- Dependency Graph and Pruning
- Shared TypeScript Configs
- Internal Packages Pattern
- Affected Packages on PR
- Migrating From Polyrepo
- Monorepo Best Practices Checklist
- Conclusion
Workspace Setup With npm/pnpm Workspaces
Modern package managers support workspaces natively. No specialized tooling required.
{
"name": "monorepo",
"version": "1.0.0",
"private": true,
"workspaces": [
"packages/*",
"apps/*",
"tools/*"
]
}
Directory structure:
monorepo/
├── package.json
├── pnpm-workspace.yaml # pnpm uses YAML instead of package.json
├── packages/ # Shared libraries
│ ├── ui/ # React components
│ │ ├── package.json
│ │ ├── src/
│ │ └── tsconfig.json
│ ├── config/ # Shared TypeScript configs
│ ├── utils/ # Utility functions
│ └── database/ # Prisma client
├── apps/ # Applications
│ ├── web/ # Next.js web app
│ ├── api/ # Express API
│ ├── admin/ # Admin dashboard
│ └── mobile-api/ # Mobile backend
├── tools/ # Build/deploy tools
│ ├── cli/
│ └── scripts/
└── turbo.json
pnpm workspace config (recommended over npm):
# pnpm-workspace.yaml
packages:
- 'packages/*'
- 'apps/*'
- 'tools/*'
# pnpm hoists dependencies: faster installs, better discoverability
hoist-pattern:
- '*'
- '@types/*'
public-hoist-pattern:
- '@types/*'
- 'lodash'
turbo.json Pipeline Configuration
Turborepo orchestrates build tasks across workspaces using a dependency graph.
{
"$schema": "https://turborepo.org/schema.json",
"version": "1",
"extends": ["//"],
"globalDependencies": [
".env.local",
".nvmrc"
],
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", "build/**"],
"cache": true
},
"test": {
"dependsOn": ["^build"],
"outputs": ["coverage/**"],
"cache": true,
"inputs": ["src/**/*.tsx", "src/**/*.ts", "test/**"]
},
"lint": {
"outputs": [],
"cache": true,
"inputs": ["src/**/*.tsx", "src/**/*.ts", "*.json"]
},
"type-check": {
"dependsOn": ["^build"],
"cache": true
},
"dev": {
"cache": false,
"persistent": true,
"outputs": ["dist/**"]
},
"deploy": {
"dependsOn": ["^build"],
"outputs": [],
"cache": false
},
"api#dev": {
"cache": false,
"persistent": true,
"dependsOn": ["database#migrate"]
},
"database#migrate": {
"cache": false,
"persistent": true
}
},
"remoteCache": {
"enabled": true
}
}
Task naming conventions:
package#task: run task only in package^task: run task in dependencies first- No prefix: run task in all packages
Remote Caching With Vercel
Turborepo's remote cache speeds CI/CD by sharing build artifacts across machines.
# Authenticate with Vercel
turbo login
# Link to team
turbo link --team=your-team
Cache outputs go to Vercel by default:
// turbo.json
{
"remoteCache": {
"enabled": true
}
}
Self-hosted remote cache:
# docker-compose.yml
version: '3.8'
services:
turbo-cache:
image: turbo-remote-cache:latest
ports:
- '3000:3000'
environment:
DATABASE_URL: postgresql://user:pass@postgres:5432/turbo_cache
depends_on:
- postgres
postgres:
image: postgres:15-alpine
environment:
POSTGRES_USER: user
POSTGRES_PASSWORD: pass
POSTGRES_DB: turbo_cache
volumes:
- postgres_data:/var/lib/postgresql/data
volumes:
postgres_data:
GitHub Actions with remote cache:
# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
steps:
- uses: actions/checkout@v3
- name: Setup Node
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'pnpm'
- name: Install pnpm
uses: pnpm/action-setup@v2
- name: Install
run: pnpm install
- name: Build and test
run: pnpm turbo run build test lint
Dependency Graph and Pruning
Turborepo builds dependency graphs to optimize builds.
# See visual graph
turbo --graph
# Build only affected by changes on PR
turbo run build --filter="...[origin/main]"
# Build package and affected dependents
turbo run build --filter="...{packages/ui}[HEAD^]"
Pruning for Docker (build only needed packages):
# Dockerfile
FROM node:18 AS pruner
WORKDIR /app
COPY . .
RUN npm install -g turbo
RUN turbo prune --scope=apps/web --docker
FROM node:18 AS builder
WORKDIR /app
COPY /app/out/json .
COPY /app/out/pnpm-lock.yaml ./pnpm-lock.yaml
RUN npm install -g pnpm
RUN pnpm install
COPY /app/out/full .
RUN pnpm turbo run build --filter=web
FROM node:18-alpine
WORKDIR /app
COPY /app/apps/web/dist .
EXPOSE 3000
CMD ["node", "server.js"]
Shared TypeScript Configs
Central config eliminates duplication:
// packages/config/tsconfig.base.json
{
"compilerOptions": {
"target": "ES2020",
"lib": ["ES2020"],
"module": "ESNext",
"strict": true,
"declaration": true,
"sourceMap": true,
"baseUrl": ".",
"paths": {
"@monorepo/ui": ["../ui/src"],
"@monorepo/utils": ["../utils/src"],
"@monorepo/database": ["../database/src"]
}
}
}
Per-package extends base:
// apps/web/tsconfig.json
{
"extends": "../../packages/config/tsconfig.base.json",
"compilerOptions": {
"jsx": "react-jsx",
"paths": {
"@/*": ["./src/*"],
"@monorepo/*": ["../../packages/*/src"]
}
},
"include": ["src"]
}
Internal Packages Pattern
Shared libraries as internal packages with versioning.
// packages/ui/package.json
{
"name": "@monorepo/ui",
"version": "1.0.0",
"private": true,
"main": "dist/index.js",
"types": "dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"./button": {
"types": "./dist/Button.d.ts",
"default": "./dist/Button.js"
}
},
"dependencies": {
"@monorepo/utils": "workspace:*"
},
"devDependencies": {
"react": "^18.0.0"
}
}
Usage in apps:
// apps/web/package.json
{
"dependencies": {
"@monorepo/ui": "workspace:*",
"@monorepo/database": "workspace:*"
}
}
TypeScript paths for direct imports:
// apps/web/src/App.tsx
import { Button } from '@monorepo/ui';
import { Button as ButtonComponent } from '../../packages/ui/src/Button';
// Both work; first is cleaner with path aliases
Affected Packages on PR
Only test/build what changed:
# Changed files vs origin/main
turbo run build --filter="...[origin/main]"
# Specific package and dependents
turbo run test --filter="...{packages/database}[HEAD~1]"
# List affected without running
turbo run --dry=json --filter="...[origin/main]" | jq '.tasks[].package'
GitHub Actions for affected:
# .github/workflows/affected.yml
name: Test Affected
on: [pull_request]
jobs:
affected:
runs-on: ubuntu-latest
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0 # Full history for git diff
- uses: pnpm/action-setup@v2
- uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'pnpm'
- run: pnpm install
- run: pnpm turbo run build test --filter="...[origin/${{ github.base_ref }}]"
Migrating From Polyrepo
Converting separate repos to monorepo:
# 1. Create monorepo structure
mkdir monorepo && cd monorepo
git init
pnpm init -y
# 2. Add workspaces
pnpm install -w
mkdir -p packages apps
# 3. Migrate first package
cd packages
git subtree add --prefix packages/utils \
https://github.com/org/repo-utils.git main
# 4. Update import paths in migrated package
# From: import { foo } from '../../utils/src'
# To: import { foo } from '@monorepo/utils'
# 5. Setup Turborepo
npm install -g turbo
turbo init
# 6. Configure turbo.json
# 7. Update CI/CD to use turbo
# Repeat for each package
Parallel migration using git subtree:
#!/bin/bash
# scripts/migrate-repos.sh
REPOS=(
"repo-utils:packages/utils"
"repo-api:apps/api"
"repo-ui:packages/ui"
"repo-web:apps/web"
)
for repo in "${REPOS[@]}"; do
IFS=':' read -r source_repo target_path <<< "$repo"
git subtree add \
--prefix="$target_path" \
"https://github.com/org/$source_repo.git" \
main --squash
echo "Migrated $source_repo to $target_path"
done
echo "Migration complete. Run 'pnpm install && turbo run build' to verify"
Monorepo Best Practices Checklist
## Monorepo Scaling Checklist
### Structure
- [ ] Clear separation: apps/ (deployed), packages/ (libraries), tools/ (build)
- [ ] One concept per package (single responsibility)
- [ ] Shared configs in dedicated packages/config package
- [ ] README in each package documenting purpose and dependencies
### Build System
- [ ] turbo.json defines all tasks with correct dependsOn
- [ ] Outputs specified correctly for caching
- [ ] Remote caching enabled (Vercel or self-hosted)
- [ ] CI uses affected filters: --filter="...[origin/main]"
- [ ] Local turbo cache cleared in CI (--no-cache)
### Dependencies
- [ ] Internal packages use workspace:* protocol
- [ ] No circular dependencies (turbo validates)
- [ ] Path aliases configured in tsconfig.base.json
- [ ] External dependencies pinned in root package-lock
- [ ] pnpm workspaces with hoist-pattern configured
### Testing and Linting
- [ ] Test runs respect workspace boundaries
- [ ] Linting shares eslint config from packages/config
- [ ] Type checking runs on full repo
- [ ] Coverage reports aggregated
### Releasing
- [ ] Versioning strategy decided (semver, independent per package)
- [ ] Changelog auto-generated from commits
- [ ] Only changed packages published (if using npm)
- [ ] Git tags created for releases
Conclusion
Monorepos with Turborepo scale from 10 packages to 100+. The key is proper configuration: clear workspace structure, task pipeline definition, shared configs, and remote caching.
Start with one monorepo. Add structure as it grows. Use Turborepo's intelligent scheduling to keep builds fast even as complexity grows.
Stop maintaining 10 separate CI/CD pipelines. Build once, cache forever.