Back to Blog
MonorepoDevOpsToolingBest Practices

Monorepo Management: Tools, Patterns, and Best Practices

Master monorepo development with modern tools. From Turborepo to Nx, learn how to manage multiple packages efficiently.

B
Bootspring Team
Engineering
June 5, 2025
6 min read

A monorepo contains multiple projects in a single repository. When managed well, monorepos enable code sharing, atomic changes, and simplified dependency management. When managed poorly, they become slow and unwieldy. Here's how to get it right.

Why Monorepos?#

Benefits#

✓ Atomic changes across packages ✓ Single source of truth ✓ Simplified dependency management ✓ Easier code sharing ✓ Consistent tooling ✓ Better visibility

Challenges#

✗ Build times can grow ✗ CI complexity increases ✗ Git operations slow down ✗ Ownership can be unclear ✗ Learning curve for tools

Monorepo Structure#

Typical Layout#

my-monorepo/ ├── apps/ │ ├── web/ # Next.js web app │ ├── mobile/ # React Native app │ └── api/ # Node.js API ├── packages/ │ ├── ui/ # Shared component library │ ├── config/ # Shared configs (ESLint, TS) │ ├── utils/ # Shared utilities │ └── database/ # Database client ├── turbo.json # Turborepo config ├── package.json # Root package.json └── pnpm-workspace.yaml # Workspace definition

Workspace Definition#

# pnpm-workspace.yaml packages: - 'apps/*' - 'packages/*'
1// package.json (root) 2{ 3 "name": "my-monorepo", 4 "private": true, 5 "workspaces": [ 6 "apps/*", 7 "packages/*" 8 ], 9 "scripts": { 10 "build": "turbo run build", 11 "dev": "turbo run dev", 12 "test": "turbo run test", 13 "lint": "turbo run lint" 14 }, 15 "devDependencies": { 16 "turbo": "^1.10.0" 17 } 18}

Build Tools#

Turborepo#

1// turbo.json 2{ 3 "$schema": "https://turbo.build/schema.json", 4 "globalDependencies": ["**/.env"], 5 "pipeline": { 6 "build": { 7 "dependsOn": ["^build"], 8 "outputs": ["dist/**", ".next/**"] 9 }, 10 "test": { 11 "dependsOn": ["build"], 12 "outputs": [] 13 }, 14 "lint": { 15 "outputs": [] 16 }, 17 "dev": { 18 "cache": false, 19 "persistent": true 20 } 21 } 22}

Key features:

  • Task caching (local and remote)
  • Parallel execution
  • Dependency graph awareness
  • Incremental builds

Nx#

1// nx.json 2{ 3 "targetDefaults": { 4 "build": { 5 "dependsOn": ["^build"], 6 "cache": true 7 }, 8 "test": { 9 "cache": true 10 } 11 }, 12 "affected": { 13 "defaultBase": "main" 14 } 15}

Key features:

  • Computation caching
  • Distributed task execution
  • Project graph visualization
  • Code generators

Package Management#

Internal Dependencies#

1// apps/web/package.json 2{ 3 "name": "@myorg/web", 4 "dependencies": { 5 "@myorg/ui": "workspace:*", 6 "@myorg/utils": "workspace:*" 7 } 8}

Shared Configurations#

1// packages/config/eslint-config.js 2module.exports = { 3 extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'], 4 rules: { 5 // Shared rules 6 }, 7}; 8 9// apps/web/.eslintrc.js 10module.exports = { 11 extends: ['@myorg/eslint-config'], 12 rules: { 13 // App-specific overrides 14 }, 15};

TypeScript Configuration#

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 } 12} 13 14// apps/web/tsconfig.json 15{ 16 "extends": "@myorg/config/tsconfig.base.json", 17 "compilerOptions": { 18 "outDir": "./dist" 19 }, 20 "include": ["src"] 21}

Task Orchestration#

Running Commands#

1# Run in all packages 2pnpm --filter '*' build 3 4# Run in specific package 5pnpm --filter @myorg/web dev 6 7# Run in package and dependencies 8pnpm --filter @myorg/web... build 9 10# Run in changed packages 11pnpm --filter '...[HEAD~1]' test

With Turborepo#

1# Run build with caching 2turbo run build 3 4# Run for specific app 5turbo run build --filter=@myorg/web 6 7# Run affected by changes 8turbo run test --filter='[HEAD~1]' 9 10# Parallel dev servers 11turbo run dev

Caching#

Local Caching#

1# Turborepo caches in node_modules/.cache/turbo 2# Second run is instant if inputs unchanged 3 4$ turbo run build 5• Packages in scope: @myorg/ui, @myorg/web 6• Running build in 2 packages 7@myorg/ui:build: cache miss, executing 8@myorg/web:build: cache miss, executing 9 10$ turbo run build 11• Running build in 2 packages 12@myorg/ui:build: cache hit, replaying output 13@myorg/web:build: cache hit, replaying output

Remote Caching#

1# Turborepo Remote Cache 2npx turbo login 3npx turbo link 4 5# Or self-hosted 6turbo run build --api="https://cache.mycompany.com" --token="xxx"

CI/CD Optimization#

Affected-Only Builds#

1# .github/workflows/ci.yml 2name: CI 3 4on: 5 pull_request: 6 7jobs: 8 build: 9 runs-on: ubuntu-latest 10 steps: 11 - uses: actions/checkout@v4 12 with: 13 fetch-depth: 0 14 15 - uses: pnpm/action-setup@v2 16 17 - uses: actions/cache@v3 18 with: 19 path: node_modules/.cache/turbo 20 key: turbo-${{ runner.os }}-${{ github.sha }} 21 restore-keys: turbo-${{ runner.os }}- 22 23 - run: pnpm install 24 25 # Only test affected packages 26 - run: pnpm turbo run test --filter='[origin/main...HEAD]'

Parallel Jobs#

1jobs: 2 build: 3 strategy: 4 matrix: 5 package: [web, api, mobile] 6 steps: 7 - run: pnpm turbo run build --filter=@myorg/${{ matrix.package }}

Code Sharing Patterns#

Shared Components#

1// packages/ui/src/Button.tsx 2export interface ButtonProps { 3 variant: 'primary' | 'secondary'; 4 children: React.ReactNode; 5 onClick?: () => void; 6} 7 8export function Button({ variant, children, onClick }: ButtonProps) { 9 return ( 10 <button className={`btn btn-${variant}`} onClick={onClick}> 11 {children} 12 </button> 13 ); 14} 15 16// packages/ui/src/index.ts 17export * from './Button'; 18export * from './Input'; 19export * from './Card';
1// apps/web/src/pages/index.tsx 2import { Button } from '@myorg/ui'; 3 4export default function Home() { 5 return <Button variant="primary">Click me</Button>; 6}

Shared Types#

1// packages/types/src/user.ts 2export interface User { 3 id: string; 4 email: string; 5 name: string; 6} 7 8// apps/api/src/routes/users.ts 9import type { User } from '@myorg/types'; 10 11// apps/web/src/hooks/useUser.ts 12import type { User } from '@myorg/types';

Versioning and Publishing#

Changesets#

1# Install changesets 2pnpm add -D @changesets/cli 3 4# Initialize 5pnpm changeset init 6 7# Create changeset 8pnpm changeset 9# Interactive prompts for version bumps 10 11# Version packages 12pnpm changeset version 13 14# Publish 15pnpm changeset publish
1# .changeset/config.json 2{ 3 "$schema": "https://unpkg.com/@changesets/config/schema.json", 4 "changelog": "@changesets/cli/changelog", 5 "commit": false, 6 "access": "restricted", 7 "baseBranch": "main" 8}

Best Practices#

Ownership#

# CODEOWNERS /apps/web/ @frontend-team /apps/api/ @backend-team /packages/ui/ @design-system-team /packages/database/ @platform-team

Consistent Scripts#

1// Every package has the same scripts 2{ 3 "scripts": { 4 "build": "...", 5 "dev": "...", 6 "test": "...", 7 "lint": "...", 8 "typecheck": "..." 9 } 10}

Dependency Hygiene#

1# Find unused dependencies 2pnpm dlx depcheck 3 4# Update all dependencies 5pnpm update -r 6 7# Check for mismatched versions 8pnpm dedupe --check

Documentation#

1# CONTRIBUTING.md 2 3## Adding a New Package 4 51. Create directory in `packages/` or `apps/` 62. Add `package.json` with standard scripts 73. Add to `pnpm-workspace.yaml` if using different pattern 84. Run `pnpm install` 9 10## Development 11 12- `pnpm dev` - Start all dev servers 13- `pnpm build` - Build all packages 14- `pnpm test` - Run all tests 15 16## Creating a Changeset 17 181. Run `pnpm changeset` 192. Select affected packages 203. Describe changes 214. Commit the changeset file

Conclusion#

Monorepos shine when you need tight coordination between packages, shared tooling, and atomic changes. Modern tools like Turborepo and Nx make them practical even at scale.

Start with a clear structure, invest in shared configurations, and leverage caching aggressively. The upfront investment pays dividends in developer productivity and code quality.

The key is treating the monorepo as a product—it needs maintenance, documentation, and continuous improvement to serve your teams well.

Share this article

Help spread the word about Bootspring