Back to Blog
MonorepoTurborepoBuild ToolsDevOps

Monorepo Setup with Turborepo

Set up efficient monorepos with Turborepo. From workspace configuration to caching to CI/CD optimization.

B
Bootspring Team
Engineering
February 4, 2022
5 min read

Monorepos simplify code sharing across multiple packages. Turborepo makes them fast with intelligent caching.

Project Structure#

my-monorepo/ ├── apps/ │ ├── web/ # Next.js app │ │ ├── package.json │ │ └── src/ │ ├── api/ # Express API │ │ ├── package.json │ │ └── src/ │ └── mobile/ # React Native app │ ├── package.json │ └── src/ ├── packages/ │ ├── ui/ # Shared UI components │ │ ├── package.json │ │ └── src/ │ ├── utils/ # Shared utilities │ │ ├── package.json │ │ └── src/ │ ├── config/ # Shared configs │ │ ├── eslint/ │ │ └── typescript/ │ └── database/ # Prisma client │ ├── package.json │ └── prisma/ ├── package.json ├── turbo.json └── pnpm-workspace.yaml

Root Configuration#

# pnpm-workspace.yaml packages: - 'apps/*' - 'packages/*'
1// package.json (root) 2{ 3 "name": "my-monorepo", 4 "private": true, 5 "scripts": { 6 "build": "turbo build", 7 "dev": "turbo dev", 8 "lint": "turbo lint", 9 "test": "turbo test", 10 "clean": "turbo clean && rm -rf node_modules", 11 "format": "prettier --write \"**/*.{ts,tsx,md}\"" 12 }, 13 "devDependencies": { 14 "prettier": "^3.0.0", 15 "turbo": "^2.0.0" 16 }, 17 "packageManager": "pnpm@8.15.0" 18}
1// turbo.json 2{ 3 "$schema": "https://turbo.build/schema.json", 4 "globalDependencies": ["**/.env.*local"], 5 "globalEnv": ["NODE_ENV"], 6 "tasks": { 7 "build": { 8 "dependsOn": ["^build"], 9 "inputs": ["$TURBO_DEFAULT$", ".env*"], 10 "outputs": [".next/**", "!.next/cache/**", "dist/**"] 11 }, 12 "dev": { 13 "cache": false, 14 "persistent": true 15 }, 16 "lint": { 17 "dependsOn": ["^build"], 18 "inputs": ["$TURBO_DEFAULT$", ".eslintrc*"] 19 }, 20 "test": { 21 "dependsOn": ["build"], 22 "inputs": ["$TURBO_DEFAULT$", "**/*.test.*"], 23 "outputs": ["coverage/**"] 24 }, 25 "clean": { 26 "cache": false 27 } 28 } 29}

Shared Packages#

1// packages/ui/package.json 2{ 3 "name": "@repo/ui", 4 "version": "0.0.0", 5 "private": true, 6 "main": "./src/index.ts", 7 "types": "./src/index.ts", 8 "exports": { 9 ".": "./src/index.ts", 10 "./button": "./src/button.tsx", 11 "./card": "./src/card.tsx" 12 }, 13 "scripts": { 14 "lint": "eslint src/", 15 "build": "tsup src/index.ts --format cjs,esm --dts", 16 "dev": "tsup src/index.ts --format cjs,esm --dts --watch" 17 }, 18 "dependencies": { 19 "react": "^18.2.0" 20 }, 21 "devDependencies": { 22 "@repo/typescript-config": "workspace:*", 23 "@repo/eslint-config": "workspace:*", 24 "tsup": "^8.0.0", 25 "typescript": "^5.0.0" 26 } 27}
1// packages/ui/src/button.tsx 2import { forwardRef, ButtonHTMLAttributes } from 'react'; 3 4export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> { 5 variant?: 'primary' | 'secondary' | 'outline'; 6 size?: 'sm' | 'md' | 'lg'; 7} 8 9export const Button = forwardRef<HTMLButtonElement, ButtonProps>( 10 ({ variant = 'primary', size = 'md', className, ...props }, ref) => { 11 return ( 12 <button 13 ref={ref} 14 className={`btn btn-${variant} btn-${size} ${className}`} 15 {...props} 16 /> 17 ); 18 } 19); 20 21Button.displayName = 'Button';
// packages/ui/src/index.ts export * from './button'; export * from './card'; export * from './input';

Shared Configuration#

1// packages/config/typescript/package.json 2{ 3 "name": "@repo/typescript-config", 4 "version": "0.0.0", 5 "private": true, 6 "files": ["base.json", "nextjs.json", "react-library.json"] 7}
1// packages/config/typescript/base.json 2{ 3 "$schema": "https://json.schemastore.org/tsconfig", 4 "compilerOptions": { 5 "strict": true, 6 "esModuleInterop": true, 7 "skipLibCheck": true, 8 "forceConsistentCasingInFileNames": true, 9 "moduleResolution": "bundler", 10 "resolveJsonModule": true, 11 "isolatedModules": true, 12 "incremental": true 13 } 14} 15 16// packages/config/typescript/nextjs.json 17{ 18 "$schema": "https://json.schemastore.org/tsconfig", 19 "extends": "./base.json", 20 "compilerOptions": { 21 "lib": ["dom", "dom.iterable", "esnext"], 22 "jsx": "preserve", 23 "module": "esnext", 24 "target": "es2017", 25 "plugins": [{ "name": "next" }] 26 } 27}
1// packages/config/eslint/next.js 2module.exports = { 3 extends: ['next/core-web-vitals', './base.js'], 4 rules: { 5 '@next/next/no-html-link-for-pages': 'off', 6 }, 7}; 8 9// packages/config/eslint/base.js 10module.exports = { 11 extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier'], 12 parser: '@typescript-eslint/parser', 13 plugins: ['@typescript-eslint'], 14 rules: { 15 '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }], 16 }, 17};

App Configuration#

1// apps/web/package.json 2{ 3 "name": "@repo/web", 4 "version": "0.0.0", 5 "private": true, 6 "scripts": { 7 "dev": "next dev", 8 "build": "next build", 9 "start": "next start", 10 "lint": "next lint" 11 }, 12 "dependencies": { 13 "@repo/ui": "workspace:*", 14 "@repo/utils": "workspace:*", 15 "next": "^14.0.0", 16 "react": "^18.2.0", 17 "react-dom": "^18.2.0" 18 }, 19 "devDependencies": { 20 "@repo/typescript-config": "workspace:*", 21 "@repo/eslint-config": "workspace:*", 22 "typescript": "^5.0.0" 23 } 24}
1// apps/web/tsconfig.json 2{ 3 "extends": "@repo/typescript-config/nextjs.json", 4 "compilerOptions": { 5 "baseUrl": ".", 6 "paths": { 7 "@/*": ["./src/*"] 8 } 9 }, 10 "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 11 "exclude": ["node_modules"] 12}
1// apps/web/src/app/page.tsx 2import { Button, Card } from '@repo/ui'; 3import { formatDate } from '@repo/utils'; 4 5export default function Home() { 6 return ( 7 <main> 8 <Card> 9 <h1>Welcome</h1> 10 <p>Today is {formatDate(new Date())}</p> 11 <Button variant="primary">Get Started</Button> 12 </Card> 13 </main> 14 ); 15}

Caching and CI/CD#

1// turbo.json with remote caching 2{ 3 "$schema": "https://turbo.build/schema.json", 4 "remoteCache": { 5 "signature": true 6 }, 7 "tasks": { 8 "build": { 9 "dependsOn": ["^build"], 10 "outputs": [".next/**", "!.next/cache/**", "dist/**"], 11 "env": ["NEXT_PUBLIC_API_URL"] 12 } 13 } 14}
1# .github/workflows/ci.yml 2name: CI 3 4on: 5 push: 6 branches: [main] 7 pull_request: 8 branches: [main] 9 10env: 11 TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} 12 TURBO_TEAM: ${{ vars.TURBO_TEAM }} 13 14jobs: 15 build: 16 runs-on: ubuntu-latest 17 18 steps: 19 - uses: actions/checkout@v4 20 21 - uses: pnpm/action-setup@v2 22 with: 23 version: 8 24 25 - uses: actions/setup-node@v4 26 with: 27 node-version: 20 28 cache: 'pnpm' 29 30 - name: Install dependencies 31 run: pnpm install --frozen-lockfile 32 33 - name: Build 34 run: pnpm build 35 36 - name: Lint 37 run: pnpm lint 38 39 - name: Test 40 run: pnpm test 41 42 deploy: 43 needs: build 44 runs-on: ubuntu-latest 45 if: github.ref == 'refs/heads/main' 46 47 steps: 48 - uses: actions/checkout@v4 49 50 - uses: pnpm/action-setup@v2 51 with: 52 version: 8 53 54 - name: Install dependencies 55 run: pnpm install --frozen-lockfile 56 57 - name: Deploy web app 58 run: pnpm --filter @repo/web deploy

Filtering and Running Tasks#

1# Run tasks for specific packages 2pnpm --filter @repo/web dev 3pnpm --filter @repo/api build 4 5# Run tasks for packages and dependencies 6pnpm --filter @repo/web... build 7 8# Run tasks for dependents 9pnpm --filter ...@repo/ui build 10 11# Run in all packages except one 12pnpm --filter '!@repo/mobile' build 13 14# Using turbo directly 15turbo build --filter=@repo/web 16turbo dev --filter=@repo/web... 17 18# Affected packages (since base branch) 19turbo build --filter='[origin/main]'

Best Practices#

Structure: ✓ Separate apps and packages ✓ Share configs as packages ✓ Use workspace protocol ✓ Keep packages focused Performance: ✓ Enable remote caching ✓ Configure proper outputs ✓ Use incremental builds ✓ Parallelize where possible Dependencies: ✓ Hoist common dependencies ✓ Version shared packages together ✓ Document breaking changes ✓ Test cross-package changes

Conclusion#

Turborepo makes monorepos efficient with intelligent caching and parallel execution. Structure apps and packages clearly, share configurations, and leverage remote caching for CI/CD. The initial setup investment pays off with improved code sharing and build performance.

Share this article

Help spread the word about Bootspring