Back to Blog
MonorepoTurborepopnpmBuild Tools

Monorepo Tooling: A Practical Guide

Set up and manage monorepos effectively. From workspace configuration to build caching to CI optimization.

B
Bootspring Team
Engineering
September 28, 2022
5 min read

Monorepos consolidate multiple projects in one repository. Here's how to set up and manage them effectively.

Why Monorepos#

Benefits: - Shared code and dependencies - Atomic changes across packages - Consistent tooling and config - Simplified dependency management Challenges: - Build complexity - CI performance - Code ownership - Repository size

Workspace Setup with pnpm#

# pnpm-workspace.yaml packages: - 'apps/*' - 'packages/*'
project/ ├── apps/ │ ├── web/ │ │ └── package.json │ └── api/ │ └── package.json ├── packages/ │ ├── ui/ │ │ └── package.json │ ├── utils/ │ │ └── package.json │ └── config/ │ └── package.json ├── package.json └── pnpm-workspace.yaml
1// Root package.json 2{ 3 "name": "monorepo", 4 "private": true, 5 "scripts": { 6 "build": "turbo run build", 7 "dev": "turbo run dev", 8 "lint": "turbo run lint", 9 "test": "turbo run test" 10 }, 11 "devDependencies": { 12 "turbo": "^1.10.0" 13 } 14} 15 16// apps/web/package.json 17{ 18 "name": "@myapp/web", 19 "dependencies": { 20 "@myapp/ui": "workspace:*", 21 "@myapp/utils": "workspace:*" 22 } 23} 24 25// packages/ui/package.json 26{ 27 "name": "@myapp/ui", 28 "main": "./dist/index.js", 29 "types": "./dist/index.d.ts", 30 "dependencies": { 31 "@myapp/utils": "workspace:*" 32 } 33}

Turborepo Configuration#

1// turbo.json 2{ 3 "$schema": "https://turbo.build/schema.json", 4 "globalDependencies": ["**/.env.*local"], 5 "pipeline": { 6 "build": { 7 "dependsOn": ["^build"], 8 "outputs": ["dist/**", ".next/**"] 9 }, 10 "lint": { 11 "outputs": [] 12 }, 13 "test": { 14 "dependsOn": ["build"], 15 "outputs": [] 16 }, 17 "dev": { 18 "cache": false, 19 "persistent": true 20 }, 21 "clean": { 22 "cache": false 23 } 24 } 25}
1# Build all packages 2pnpm turbo run build 3 4# Build specific package 5pnpm turbo run build --filter=@myapp/web 6 7# Build package and dependencies 8pnpm turbo run build --filter=@myapp/web... 9 10# Build only changed packages 11pnpm turbo run build --filter=[HEAD^1]

Shared Configuration#

1// packages/config/eslint.js 2module.exports = { 3 extends: [ 4 'eslint:recommended', 5 'plugin:@typescript-eslint/recommended', 6 'prettier', 7 ], 8 parser: '@typescript-eslint/parser', 9 plugins: ['@typescript-eslint'], 10 rules: { 11 '@typescript-eslint/no-unused-vars': 'error', 12 }, 13}; 14 15// apps/web/.eslintrc.js 16module.exports = { 17 root: true, 18 extends: ['@myapp/config/eslint'], 19 parserOptions: { 20 project: './tsconfig.json', 21 }, 22};
1// packages/config/tsconfig.base.json 2{ 3 "compilerOptions": { 4 "target": "ES2020", 5 "module": "ESNext", 6 "moduleResolution": "bundler", 7 "strict": true, 8 "esModuleInterop": true, 9 "skipLibCheck": true, 10 "declaration": true, 11 "declarationMap": true, 12 "sourceMap": true 13 } 14} 15 16// apps/web/tsconfig.json 17{ 18 "extends": "@myapp/config/tsconfig.base.json", 19 "compilerOptions": { 20 "outDir": "dist" 21 }, 22 "include": ["src"] 23}

Internal Packages#

1// packages/ui/src/index.ts 2export { Button } from './Button'; 3export { Input } from './Input'; 4export { Card } from './Card'; 5export type { ButtonProps, InputProps } from './types'; 6 7// packages/ui/src/Button.tsx 8import { cn } from '@myapp/utils'; 9 10export interface ButtonProps { 11 children: React.ReactNode; 12 variant?: 'primary' | 'secondary'; 13 onClick?: () => void; 14} 15 16export function Button({ children, variant = 'primary', onClick }: ButtonProps) { 17 return ( 18 <button 19 className={cn( 20 'px-4 py-2 rounded', 21 variant === 'primary' && 'bg-blue-500 text-white', 22 variant === 'secondary' && 'bg-gray-200 text-gray-800' 23 )} 24 onClick={onClick} 25 > 26 {children} 27 </button> 28 ); 29}
1// packages/ui/package.json 2{ 3 "name": "@myapp/ui", 4 "version": "0.0.0", 5 "main": "./dist/index.js", 6 "module": "./dist/index.mjs", 7 "types": "./dist/index.d.ts", 8 "exports": { 9 ".": { 10 "import": "./dist/index.mjs", 11 "require": "./dist/index.js", 12 "types": "./dist/index.d.ts" 13 } 14 }, 15 "scripts": { 16 "build": "tsup src/index.ts --format cjs,esm --dts", 17 "dev": "tsup src/index.ts --format cjs,esm --dts --watch" 18 } 19}

Build Caching#

1// turbo.json with remote caching 2{ 3 "pipeline": { 4 "build": { 5 "outputs": ["dist/**"], 6 "cache": true 7 } 8 } 9}
1# Enable remote caching 2npx turbo login 3npx turbo link 4 5# Or use environment variables 6TURBO_TOKEN=xxx 7TURBO_TEAM=myteam
1# GitHub Actions with caching 2- name: Setup Turbo cache 3 uses: actions/cache@v3 4 with: 5 path: .turbo 6 key: turbo-${{ github.sha }} 7 restore-keys: | 8 turbo- 9 10- name: Build 11 run: pnpm turbo run build

CI Configuration#

1# .github/workflows/ci.yml 2name: CI 3 4on: 5 push: 6 branches: [main] 7 pull_request: 8 branches: [main] 9 10jobs: 11 build: 12 runs-on: ubuntu-latest 13 steps: 14 - uses: actions/checkout@v4 15 with: 16 fetch-depth: 2 17 18 - uses: pnpm/action-setup@v2 19 with: 20 version: 8 21 22 - uses: actions/setup-node@v4 23 with: 24 node-version: 20 25 cache: 'pnpm' 26 27 - name: Install dependencies 28 run: pnpm install 29 30 - name: Build changed packages 31 run: pnpm turbo run build --filter='[HEAD^1]' 32 33 - name: Lint 34 run: pnpm turbo run lint --filter='[HEAD^1]' 35 36 - name: Test 37 run: pnpm turbo run test --filter='[HEAD^1]' 38 39 deploy-web: 40 needs: build 41 if: github.ref == 'refs/heads/main' 42 runs-on: ubuntu-latest 43 steps: 44 - uses: actions/checkout@v4 45 46 - uses: pnpm/action-setup@v2 47 with: 48 version: 8 49 50 - run: pnpm install 51 - run: pnpm turbo run build --filter=@myapp/web... 52 - run: pnpm --filter=@myapp/web deploy

Versioning and Publishing#

1// Using changesets 2{ 3 "devDependencies": { 4 "@changesets/cli": "^2.26.0" 5 }, 6 "scripts": { 7 "changeset": "changeset", 8 "version": "changeset version", 9 "publish": "turbo run build && changeset publish" 10 } 11}
1# .changeset/config.json 2{ 3 "changelog": "@changesets/cli/changelog", 4 "commit": false, 5 "fixed": [], 6 "linked": [], 7 "access": "restricted", 8 "baseBranch": "main", 9 "updateInternalDependencies": "patch" 10}
1# Create a changeset 2pnpm changeset 3 4# Version packages 5pnpm changeset version 6 7# Publish packages 8pnpm changeset publish

Task Dependencies#

1// turbo.json 2{ 3 "pipeline": { 4 "build": { 5 "dependsOn": ["^build"], 6 "outputs": ["dist/**"] 7 }, 8 "deploy": { 9 "dependsOn": ["build", "test", "lint"] 10 }, 11 "test": { 12 "dependsOn": ["build"] 13 }, 14 "db:generate": { 15 "cache": false 16 }, 17 "db:push": { 18 "cache": false, 19 "dependsOn": ["db:generate"] 20 } 21 } 22}

Best Practices#

Structure: ✓ Use consistent package naming (@scope/name) ✓ Keep shared config in dedicated package ✓ Organize by feature or layer ✓ Document package dependencies Performance: ✓ Enable build caching ✓ Use remote cache in CI ✓ Filter builds to changed packages ✓ Parallelize independent tasks Dependencies: ✓ Use workspace protocol (workspace:*) ✓ Hoist shared dependencies ✓ Keep package versions aligned ✓ Avoid circular dependencies CI/CD: ✓ Cache node_modules and turbo ✓ Build only affected packages ✓ Run tests in parallel ✓ Deploy packages independently

Conclusion#

Monorepos work well with proper tooling. Use pnpm workspaces for dependency management, Turborepo for build orchestration, and changesets for versioning. Focus on caching and filtering to keep builds fast as the repository grows.

Share this article

Help spread the word about Bootspring