Back to Blog
MonorepoArchitectureBuild ToolsDevOps

Monorepo Architecture: Tools and Best Practices

Scale your codebase with monorepos. From workspace setup to build optimization to dependency management.

B
Bootspring Team
Engineering
January 5, 2024
5 min read

Monorepos keep all your code in one repository. Done right, they improve code sharing, simplify refactoring, and streamline CI/CD. Here's how to implement them effectively.

Why Monorepo?#

Benefits: ✓ Shared code without publishing ✓ Atomic changes across packages ✓ Unified tooling and config ✓ Easier refactoring ✓ Single source of truth ✓ Simplified dependency management Challenges: ✗ Longer initial clone ✗ Complex build orchestration ✗ Requires good tooling ✗ CI/CD complexity

Project Structure#

my-monorepo/ ├── apps/ │ ├── web/ # Next.js frontend │ │ ├── src/ │ │ └── package.json │ ├── api/ # Express backend │ │ ├── src/ │ │ └── package.json │ └── mobile/ # React Native │ ├── src/ │ └── package.json ├── packages/ │ ├── ui/ # Shared UI components │ │ ├── src/ │ │ └── package.json │ ├── config/ # Shared configs │ │ ├── eslint/ │ │ ├── typescript/ │ │ └── package.json │ └── utils/ # Shared utilities │ ├── src/ │ └── package.json ├── package.json # Root package.json ├── pnpm-workspace.yaml # Workspace config └── turbo.json # Turborepo config

Workspace Setup (pnpm)#

# pnpm-workspace.yaml packages: - 'apps/*' - 'packages/*'
1// Root package.json 2{ 3 "name": "my-monorepo", 4 "private": true, 5 "scripts": { 6 "dev": "turbo dev", 7 "build": "turbo build", 8 "test": "turbo test", 9 "lint": "turbo lint", 10 "clean": "turbo clean" 11 }, 12 "devDependencies": { 13 "turbo": "^2.0.0" 14 } 15}
1// apps/web/package.json 2{ 3 "name": "@myorg/web", 4 "private": true, 5 "dependencies": { 6 "@myorg/ui": "workspace:*", 7 "@myorg/utils": "workspace:*", 8 "next": "^14.0.0" 9 } 10}

Turborepo Configuration#

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

Shared Configurations#

1// packages/config/eslint/index.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 // Shared rules 12 }, 13}; 14 15// Usage in apps/web/.eslintrc.js 16module.exports = { 17 extends: ['@myorg/config/eslint'], 18 // App-specific overrides 19};
1// packages/config/typescript/base.json 2{ 3 "$schema": "https://json.schemastore.org/tsconfig", 4 "compilerOptions": { 5 "target": "ES2020", 6 "module": "ESNext", 7 "moduleResolution": "bundler", 8 "strict": true, 9 "skipLibCheck": true, 10 "declaration": true, 11 "declarationMap": true, 12 "sourceMap": true 13 } 14} 15 16// apps/web/tsconfig.json 17{ 18 "extends": "@myorg/config/typescript/nextjs.json", 19 "compilerOptions": { 20 "baseUrl": ".", 21 "paths": { 22 "@/*": ["./src/*"] 23 } 24 }, 25 "include": ["src"], 26 "exclude": ["node_modules"] 27}

Shared UI Package#

1// packages/ui/src/button.tsx 2import { forwardRef, ButtonHTMLAttributes } from 'react'; 3import { cn } from './utils'; 4 5interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> { 6 variant?: 'primary' | 'secondary' | 'outline'; 7 size?: 'sm' | 'md' | 'lg'; 8} 9 10export const Button = forwardRef<HTMLButtonElement, ButtonProps>( 11 ({ className, variant = 'primary', size = 'md', ...props }, ref) => { 12 return ( 13 <button 14 ref={ref} 15 className={cn( 16 'rounded font-medium transition-colors', 17 variants[variant], 18 sizes[size], 19 className 20 )} 21 {...props} 22 /> 23 ); 24 } 25); 26 27Button.displayName = 'Button';
1// packages/ui/package.json 2{ 3 "name": "@myorg/ui", 4 "version": "0.0.0", 5 "main": "./src/index.ts", 6 "types": "./src/index.ts", 7 "exports": { 8 ".": "./src/index.ts", 9 "./button": "./src/button.tsx", 10 "./input": "./src/input.tsx" 11 }, 12 "peerDependencies": { 13 "react": "^18.0.0" 14 } 15}

Version Management#

1# Using Changesets for versioning 2npx changeset init 3 4# Create a changeset 5npx changeset 6# Select packages that changed 7# Write changelog entry 8 9# Version packages 10npx changeset version 11 12# Publish packages 13npx 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 "fixed": [], 7 "linked": [], 8 "access": "restricted", 9 "baseBranch": "main", 10 "updateInternalDependencies": "patch" 11}

CI/CD Pipeline#

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 14 steps: 15 - uses: actions/checkout@v4 16 with: 17 fetch-depth: 2 18 19 - uses: pnpm/action-setup@v3 20 with: 21 version: 8 22 23 - uses: actions/setup-node@v4 24 with: 25 node-version: 20 26 cache: 'pnpm' 27 28 - name: Install dependencies 29 run: pnpm install --frozen-lockfile 30 31 - name: Build 32 run: pnpm build 33 34 - name: Test 35 run: pnpm test 36 37 - name: Lint 38 run: pnpm lint 39 40 # Only run affected packages 41 affected: 42 runs-on: ubuntu-latest 43 steps: 44 - uses: actions/checkout@v4 45 with: 46 fetch-depth: 0 47 48 - uses: pnpm/action-setup@v3 49 - uses: actions/setup-node@v4 50 51 - run: pnpm install --frozen-lockfile 52 53 # Only test changed packages 54 - name: Test affected 55 run: pnpm turbo test --filter='...[origin/main]'

Remote Caching#

# Turborepo remote caching with Vercel npx turbo login npx turbo link
1// turbo.json with remote caching 2{ 3 "remoteCache": { 4 "signature": true 5 } 6}

Best Practices#

DO: ✓ Use consistent tooling across packages ✓ Share configurations as packages ✓ Implement proper dependency boundaries ✓ Use remote caching for CI ✓ Only build/test affected packages ✓ Document package relationships DON'T: ✗ Create circular dependencies ✗ Share too much code (tight coupling) ✗ Skip proper versioning ✗ Ignore build performance ✗ Put everything in one package

Task Orchestration#

1# Run specific package 2pnpm --filter @myorg/web dev 3 4# Run package and dependencies 5pnpm --filter @myorg/web... build 6 7# Run dependents of a package 8pnpm --filter ...@myorg/ui build 9 10# Run affected since main 11pnpm turbo build --filter='...[origin/main]'

Conclusion#

Monorepos enable code sharing and atomic changes at scale. Use proper tooling (Turborepo, pnpm), implement remote caching, and maintain clear package boundaries.

Start simple, add complexity as needed, and invest in build performance early.

Share this article

Help spread the word about Bootspring