Architecture
Monorepo
Managing multiple packages in a single repository with shared tooling
Monorepo
A monorepo consolidates multiple packages or applications in a single repository, enabling code sharing and coordinated releases.
Benefits and Tradeoffs
Benefits
- Shared code and dependencies
- Atomic changes across packages
- Unified tooling and configuration
- Easier refactoring
- Single source of truth
Tradeoffs
- Larger repository size
- Complex build orchestration
- Requires monorepo tooling
- CI/CD complexity
- Access control challenges
Monorepo Tools
| Tool | Philosophy | Build System | Caching |
|---|---|---|---|
| Turborepo | Simple, fast | Task runner | Remote |
| Nx | Feature-rich | Integrated | Remote |
| pnpm | Package manager | Workspace | Local |
| Lerna | Publish-focused | External | Via Nx |
| Rush | Enterprise | Custom | Build cache |
Turborepo Setup
Project Structure
my-monorepo/
├── package.json
├── turbo.json
├── pnpm-workspace.yaml
├── apps/
│ ├── web/
│ │ ├── package.json
│ │ └── src/
│ └── docs/
│ ├── package.json
│ └── src/
├── packages/
│ ├── ui/
│ │ ├── package.json
│ │ └── src/
│ ├── utils/
│ │ ├── package.json
│ │ └── src/
│ └── config/
│ ├── eslint/
│ ├── typescript/
│ └── tailwind/
└── tooling/
└── scripts/Root Configuration
// package.json
{
"name": "my-monorepo",
"private": true,
"workspaces": ["apps/*", "packages/*"],
"scripts": {
"build": "turbo run build",
"dev": "turbo run dev",
"lint": "turbo run lint",
"test": "turbo run test",
"clean": "turbo run clean && rm -rf node_modules"
},
"devDependencies": {
"turbo": "^2.0.0"
},
"packageManager": "pnpm@8.15.0"
}# pnpm-workspace.yaml
packages:
- 'apps/*'
- 'packages/*'// turbo.json
{
"$schema": "https://turbo.build/schema.json",
"globalDependencies": ["**/.env.*local"],
"tasks": {
"build": {
"dependsOn": ["^build"],
"inputs": ["$TURBO_DEFAULT$", ".env*"],
"outputs": ["dist/**", ".next/**", "!.next/cache/**"]
},
"dev": {
"cache": false,
"persistent": true
},
"lint": {
"dependsOn": ["^build"],
"inputs": ["$TURBO_DEFAULT$", ".eslintrc*"]
},
"test": {
"dependsOn": ["^build"],
"inputs": ["$TURBO_DEFAULT$"]
},
"clean": {
"cache": false
}
}
}Package Configuration
// packages/ui/package.json
{
"name": "@myorg/ui",
"version": "1.0.0",
"main": "./dist/index.js",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": "./dist/index.mjs",
"require": "./dist/index.js",
"types": "./dist/index.d.ts"
},
"./button": {
"import": "./dist/button.mjs",
"require": "./dist/button.js",
"types": "./dist/button.d.ts"
}
},
"scripts": {
"build": "tsup src/index.ts --format cjs,esm --dts",
"dev": "tsup src/index.ts --format cjs,esm --dts --watch",
"lint": "eslint src/",
"clean": "rm -rf dist"
},
"devDependencies": {
"@myorg/eslint-config": "workspace:*",
"@myorg/typescript-config": "workspace:*",
"tsup": "^8.0.0",
"typescript": "^5.0.0"
},
"peerDependencies": {
"react": "^18.0.0"
}
}// apps/web/package.json
{
"name": "@myorg/web",
"version": "1.0.0",
"private": true,
"scripts": {
"build": "next build",
"dev": "next dev",
"lint": "next lint",
"start": "next start"
},
"dependencies": {
"@myorg/ui": "workspace:*",
"@myorg/utils": "workspace:*",
"next": "^14.0.0",
"react": "^18.0.0"
},
"devDependencies": {
"@myorg/eslint-config": "workspace:*",
"@myorg/typescript-config": "workspace:*"
}
}Nx Setup
Configuration
// nx.json
{
"$schema": "./node_modules/nx/schemas/nx-schema.json",
"targetDefaults": {
"build": {
"dependsOn": ["^build"],
"inputs": ["production", "^production"],
"cache": true
},
"test": {
"inputs": ["default", "^production", "{workspaceRoot}/jest.preset.js"],
"cache": true
},
"lint": {
"inputs": ["default", "{workspaceRoot}/.eslintrc.json"],
"cache": true
}
},
"namedInputs": {
"default": ["{projectRoot}/**/*", "sharedGlobals"],
"production": ["default", "!{projectRoot}/**/?(*.)+(spec|test).[jt]s?(x)"],
"sharedGlobals": []
},
"plugins": [
{
"plugin": "@nx/next/plugin",
"options": {
"buildTargetName": "build",
"devTargetName": "dev",
"startTargetName": "start"
}
}
]
}Creating Projects
# Create React library
npx nx g @nx/react:library ui --directory=packages/ui
# Create Next.js app
npx nx g @nx/next:application web --directory=apps/web
# Create shared library
npx nx g @nx/js:library utils --directory=packages/utilsRunning Tasks
# Run for specific project
nx build @myorg/web
# Run for all affected projects
nx affected -t build
# Visualize dependencies
nx graph
# Run with parallelism
nx run-many -t build test lint --parallel=5Shared Configuration
TypeScript Config
// packages/config/typescript/base.json
{
"$schema": "https://json.schemastore.org/tsconfig",
"display": "Base",
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022"],
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true
},
"exclude": ["node_modules", "dist"]
}// packages/config/typescript/react.json
{
"$schema": "https://json.schemastore.org/tsconfig",
"display": "React",
"extends": "./base.json",
"compilerOptions": {
"jsx": "react-jsx",
"lib": ["ES2022", "DOM", "DOM.Iterable"]
}
}// apps/web/tsconfig.json
{
"extends": "@myorg/typescript-config/react.json",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src", "next-env.d.ts"],
"exclude": ["node_modules"]
}ESLint Config
// packages/config/eslint/base.js
module.exports = {
env: {
es2022: true,
node: true,
},
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'prettier',
],
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint'],
rules: {
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
},
};// packages/config/eslint/react.js
module.exports = {
extends: [
'./base.js',
'plugin:react/recommended',
'plugin:react-hooks/recommended',
],
settings: {
react: { version: 'detect' },
},
rules: {
'react/react-in-jsx-scope': 'off',
},
};Versioning and Publishing
Changesets
# Install changesets
pnpm add -Dw @changesets/cli
# Initialize
pnpm changeset init// .changeset/config.json
{
"$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json",
"changelog": "@changesets/cli/changelog",
"commit": false,
"fixed": [],
"linked": [],
"access": "public",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": ["@myorg/web", "@myorg/docs"]
}# Create a changeset
pnpm changeset
# Version packages
pnpm changeset version
# Publish packages
pnpm changeset publishCI/CD for Monorepo
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 2 # For affected detection
- uses: pnpm/action-setup@v2
with:
version: 8
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
# Turborepo remote caching
- name: Build
run: pnpm build
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
- name: Test
run: pnpm test
- name: Lint
run: pnpm lint
release:
if: github.ref == 'refs/heads/main'
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v2
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
- name: Create Release Pull Request or Publish
uses: changesets/action@v1
with:
publish: pnpm changeset publish
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}Remote Caching
# Turborepo remote caching
npx turbo login
npx turbo link
# Run with remote cache
turbo build --remote-only// turbo.json with remote cache
{
"remoteCache": {
"signature": true
}
}Best Practices
Monorepo Guidelines
- Define clear package boundaries
- Use workspace protocol for internal deps
- Share configuration via packages
- Enable remote caching for CI
- Use changesets for version management
- Run affected-only checks in CI
- Keep build times fast with caching
- Document package relationships