|
| 1 | +# Monorepo Best Practices |
| 2 | + |
| 3 | +Essential patterns for structuring and maintaining a healthy Turborepo monorepo. |
| 4 | + |
| 5 | +## Repository Structure |
| 6 | + |
| 7 | +### Standard Layout |
| 8 | + |
| 9 | +``` |
| 10 | +my-monorepo/ |
| 11 | +├── apps/ # Application packages (deployable) |
| 12 | +│ ├── web/ |
| 13 | +│ ├── docs/ |
| 14 | +│ └── api/ |
| 15 | +├── packages/ # Library packages (shared code) |
| 16 | +│ ├── ui/ |
| 17 | +│ ├── utils/ |
| 18 | +│ └── config-*/ # Shared configs (eslint, typescript, etc.) |
| 19 | +├── package.json # Root package.json (minimal deps) |
| 20 | +├── turbo.json # Turborepo configuration |
| 21 | +├── pnpm-workspace.yaml # (pnpm) or workspaces in package.json |
| 22 | +└── pnpm-lock.yaml # Lockfile (required) |
| 23 | +``` |
| 24 | + |
| 25 | +### Key Principles |
| 26 | + |
| 27 | +1. **`apps/` for deployables**: Next.js sites, APIs, CLIs - things that get deployed |
| 28 | +2. **`packages/` for libraries**: Shared code consumed by apps or other packages |
| 29 | +3. **One purpose per package**: Each package should do one thing well |
| 30 | +4. **No nested packages**: Don't put packages inside packages |
| 31 | + |
| 32 | +## Package Types |
| 33 | + |
| 34 | +### Application Packages (`apps/`) |
| 35 | + |
| 36 | +- **Deployable**: These are the "endpoints" of your package graph |
| 37 | +- **Not installed by other packages**: Apps shouldn't be dependencies of other packages |
| 38 | +- **No shared code**: If code needs sharing, extract to `packages/` |
| 39 | + |
| 40 | +```json |
| 41 | +// apps/web/package.json |
| 42 | +{ |
| 43 | + "name": "web", |
| 44 | + "private": true, |
| 45 | + "dependencies": { |
| 46 | + "@repo/ui": "workspace:*", |
| 47 | + "next": "latest" |
| 48 | + } |
| 49 | +} |
| 50 | +``` |
| 51 | + |
| 52 | +### Library Packages (`packages/`) |
| 53 | + |
| 54 | +- **Shared code**: Utilities, components, configs |
| 55 | +- **Namespaced names**: Use `@repo/` or `@yourorg/` prefix |
| 56 | +- **Clear exports**: Define what the package exposes |
| 57 | + |
| 58 | +```json |
| 59 | +// packages/ui/package.json |
| 60 | +{ |
| 61 | + "name": "@repo/ui", |
| 62 | + "exports": { |
| 63 | + "./button": "./src/button.tsx", |
| 64 | + "./card": "./src/card.tsx" |
| 65 | + } |
| 66 | +} |
| 67 | +``` |
| 68 | + |
| 69 | +## Package Compilation Strategies |
| 70 | + |
| 71 | +### Just-in-Time (Simplest) |
| 72 | + |
| 73 | +Export TypeScript directly; let the app's bundler compile it. |
| 74 | + |
| 75 | +```json |
| 76 | +{ |
| 77 | + "name": "@repo/ui", |
| 78 | + "exports": { |
| 79 | + "./button": "./src/button.tsx" |
| 80 | + } |
| 81 | +} |
| 82 | +``` |
| 83 | + |
| 84 | +**Pros**: Zero build config, instant changes |
| 85 | +**Cons**: Can't cache builds, requires app bundler support |
| 86 | + |
| 87 | +### Compiled (Recommended for Libraries) |
| 88 | + |
| 89 | +Package compiles itself with `tsc` or bundler. |
| 90 | + |
| 91 | +```json |
| 92 | +{ |
| 93 | + "name": "@repo/ui", |
| 94 | + "exports": { |
| 95 | + "./button": { |
| 96 | + "types": "./src/button.tsx", |
| 97 | + "default": "./dist/button.js" |
| 98 | + } |
| 99 | + }, |
| 100 | + "scripts": { |
| 101 | + "build": "tsc" |
| 102 | + } |
| 103 | +} |
| 104 | +``` |
| 105 | + |
| 106 | +**Pros**: Cacheable by Turborepo, works everywhere |
| 107 | +**Cons**: More configuration |
| 108 | + |
| 109 | +## Dependency Management |
| 110 | + |
| 111 | +### Install Where Used |
| 112 | + |
| 113 | +Install dependencies in the package that uses them, not the root. |
| 114 | + |
| 115 | +```bash |
| 116 | +# Good: Install in the package that needs it |
| 117 | +pnpm add lodash --filter=@repo/utils |
| 118 | + |
| 119 | +# Avoid: Installing everything at root |
| 120 | +pnpm add lodash -w # Only for repo-level tools |
| 121 | +``` |
| 122 | + |
| 123 | +### Root Dependencies |
| 124 | + |
| 125 | +Only these belong in root `package.json`: |
| 126 | + |
| 127 | +- `turbo` - The build system |
| 128 | +- `husky`, `lint-staged` - Git hooks |
| 129 | +- Repository-level tooling |
| 130 | + |
| 131 | +### Internal Dependencies |
| 132 | + |
| 133 | +Use workspace protocol for internal packages: |
| 134 | + |
| 135 | +```json |
| 136 | +// pnpm/bun |
| 137 | +{ "@repo/ui": "workspace:*" } |
| 138 | + |
| 139 | +// npm/yarn |
| 140 | +{ "@repo/ui": "*" } |
| 141 | +``` |
| 142 | + |
| 143 | +## Exports Best Practices |
| 144 | + |
| 145 | +### Use `exports` Field (Not `main`) |
| 146 | + |
| 147 | +```json |
| 148 | +{ |
| 149 | + "exports": { |
| 150 | + ".": "./src/index.ts", |
| 151 | + "./button": "./src/button.tsx", |
| 152 | + "./utils": "./src/utils.ts" |
| 153 | + } |
| 154 | +} |
| 155 | +``` |
| 156 | + |
| 157 | +### Avoid Barrel Files |
| 158 | + |
| 159 | +Don't create `index.ts` files that re-export everything: |
| 160 | + |
| 161 | +```typescript |
| 162 | +// BAD: packages/ui/src/index.ts |
| 163 | +export * from './button'; |
| 164 | +export * from './card'; |
| 165 | +export * from './modal'; |
| 166 | +// ... imports everything even if you need one thing |
| 167 | + |
| 168 | +// GOOD: Direct exports in package.json |
| 169 | +{ |
| 170 | + "exports": { |
| 171 | + "./button": "./src/button.tsx", |
| 172 | + "./card": "./src/card.tsx" |
| 173 | + } |
| 174 | +} |
| 175 | +``` |
| 176 | + |
| 177 | +### Namespace Your Packages |
| 178 | + |
| 179 | +```json |
| 180 | +// Good |
| 181 | +{ "name": "@repo/ui" } |
| 182 | +{ "name": "@acme/utils" } |
| 183 | + |
| 184 | +// Avoid (conflicts with npm registry) |
| 185 | +{ "name": "ui" } |
| 186 | +{ "name": "utils" } |
| 187 | +``` |
| 188 | + |
| 189 | +## Common Anti-Patterns |
| 190 | + |
| 191 | +### Accessing Files Across Package Boundaries |
| 192 | + |
| 193 | +```typescript |
| 194 | +// BAD: Reaching into another package |
| 195 | +import { Button } from "../../packages/ui/src/button"; |
| 196 | + |
| 197 | +// GOOD: Install and import properly |
| 198 | +import { Button } from "@repo/ui/button"; |
| 199 | +``` |
| 200 | + |
| 201 | +### Shared Code in Apps |
| 202 | + |
| 203 | +``` |
| 204 | +// BAD |
| 205 | +apps/ |
| 206 | + web/ |
| 207 | + shared/ # This should be a package! |
| 208 | + utils.ts |
| 209 | +
|
| 210 | +// GOOD |
| 211 | +packages/ |
| 212 | + utils/ # Proper shared package |
| 213 | + src/utils.ts |
| 214 | +``` |
| 215 | + |
| 216 | +### Too Many Root Dependencies |
| 217 | + |
| 218 | +```json |
| 219 | +// BAD: Root has app dependencies |
| 220 | +{ |
| 221 | + "dependencies": { |
| 222 | + "react": "^18", |
| 223 | + "next": "^14", |
| 224 | + "lodash": "^4" |
| 225 | + } |
| 226 | +} |
| 227 | + |
| 228 | +// GOOD: Root only has repo tools |
| 229 | +{ |
| 230 | + "devDependencies": { |
| 231 | + "turbo": "latest", |
| 232 | + "husky": "latest" |
| 233 | + } |
| 234 | +} |
| 235 | +``` |
| 236 | + |
| 237 | +## See Also |
| 238 | + |
| 239 | +- [structure.md](./structure.md) - Detailed repository structure patterns |
| 240 | +- [packages.md](./packages.md) - Creating and managing internal packages |
| 241 | +- [dependencies.md](./dependencies.md) - Dependency management strategies |
0 commit comments