Monorepo Architecture with Nx and Turborepo
Monorepos enable code sharing and consistent tooling across multiple packages. This guide compares Nx and Turborepo and covers production patterns.
Why Monorepos?
Benefits:
- Code sharing: Shared utilities, types, and components
- Atomic commits: Change multiple packages in one commit
- Consistent tooling: One linter, formatter, TypeScript config
- Simplified dependency management: No version conflicts
Turborepo Setup
# Create new monorepo with Turborepo
npx create-turbo@latest my-monorepo
cd my-monorepo
// turbo.json
{
"$schema": "https://turbo.build/schema.json",
"globalDependencies": ["**/.env.*local"],
"pipeline": {
"build": {
"dependsOn": ["^build"], // ^ means run deps first
"outputs": ["dist/**", ".next/**"],
"cache": true
},
"test": {
"dependsOn": ["build"],
"outputs": ["coverage/**"],
"cache": true
},
"lint": {
"outputs": []
},
"dev": {
"cache": false,
"persistent": true // Long-running process
},
"typecheck": {
"dependsOn": ["^build"],
"outputs": []
}
}
}
Workspace Structure
my-monorepo/
apps/
web/ # Next.js frontend
api/ # Express backend
docs/ # Documentation site
packages/
ui/ # Shared React components
types/ # Shared TypeScript types
utils/ # Shared utilities
config/ # Shared configs (eslint, tsconfig)
package.json # Root workspace config
turbo.json
// Root package.json
{
"name": "my-monorepo",
"workspaces": ["apps/*", "packages/*"],
"scripts": {
"build": "turbo run build",
"dev": "turbo run dev",
"test": "turbo run test",
"lint": "turbo run lint",
"typecheck": "turbo run typecheck"
},
"devDependencies": {
"turbo": "latest"
}
}
Shared Package Setup
// packages/ui/package.json
{
"name": "@myapp/ui",
"main": "./src/index.ts",
"types": "./src/index.ts",
"exports": {
".": "./src/index.ts"
},
"scripts": {
"build": "tsup src/index.ts --format cjs,esm --dts",
"dev": "tsup src/index.ts --format cjs,esm --dts --watch",
"lint": "eslint src/",
"typecheck": "tsc --noEmit"
},
"peerDependencies": {
"react": ">=18"
}
}
// packages/ui/src/Button.tsx
export interface ButtonProps {
children: React.ReactNode;
variant?: 'primary' | 'secondary';
onClick?: () => void;
}
export function Button({ children, variant = 'primary', onClick }: ButtonProps) {
return (
<button
className={`btn btn-${variant}`}
onClick={onClick}
>
{children}
</button>
);
}
// packages/ui/src/index.ts
export { Button } from './Button';
export { Input } from './Input';
export { Modal } from './Modal';
// apps/web/src/page.tsx - consuming shared package
import { Button, Input } from '@myapp/ui';
export default function HomePage() {
return (
<div>
<Input placeholder="Search..." />
<Button variant="primary">Submit</Button>
</div>
);
}
Shared TypeScript Configuration
// packages/config/tsconfig.base.json
{
"compilerOptions": {
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"moduleResolution": "bundler",
"target": "ES2022",
"lib": ["ES2022"],
"paths": {
"@myapp/*": ["../../packages/*/src"]
}
}
}
// apps/web/tsconfig.json
{
"extends": "@myapp/config/tsconfig.base.json",
"compilerOptions": {
"lib": ["ES2022", "DOM"],
"plugins": [{ "name": "next" }]
},
"include": ["src/**/*", "next-env.d.ts"]
}
Nx Monorepo Alternative
# Create Nx workspace
npx create-nx-workspace@latest my-workspace
cd my-workspace
# Generate apps and libraries
nx generate @nx/react:application web
nx generate @nx/express:application api
nx generate @nx/react:library ui --directory=shared
// nx.json
{
"targetDefaults": {
"build": {
"dependsOn": ["^build"],
"cache": true
},
"test": {
"cache": true
}
},
"affected": {
"defaultBase": "main"
}
}
CI Optimization with Remote Caching
# Turborepo Remote Cache (Vercel)
npx turbo login
npx turbo link # Link to Vercel team
# GitHub Actions
- name: Build
run: turbo run build
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
# Run only affected packages
turbo run test --filter="...[HEAD^1]"
# Only packages changed since last commit
Internal Package Versioning
// packages/ui/package.json
{
"version": "0.0.0", // Internal - don't publish
// OR use changesets for publishable packages
}
# Changesets for versioning
npx changeset init
npx changeset # Create changeset for modified packages
npx changeset version # Bump versions based on changesets
npx changeset publish # Publish to npm
Summary
Monorepo best practices:
- Use Turborepo for JavaScript-focused monorepos (simpler)
- Use Nx for large enterprise codebases (more features)
- Define clear package boundaries with explicit exports
- Share tsconfig, eslint, and prettier configs
- Use Remote Cache for fast CI builds
- Use Changesets for publishing packages to npm