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 deployFiltering 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.