What is a Monorepo?
A monorepo is a single repository that contains multiple projects, packages, or applications. Instead of having separate Git repositories for your web app, API server, shared utilities, and component library, everything lives in one repository. This approach is used by Google, Meta, Microsoft, and many large open-source projects (React, Next.js, Babel, Jest).
Monorepo Benefits
- Code Sharing: Share code between packages without publishing to npm. Import directly from workspace packages.
- Atomic Changes: A single commit can update the API, shared types, and frontend simultaneously.
- Consistent Tooling: Share ESLint, Prettier, TypeScript, and test configurations across all packages.
- Simplified CI/CD: One pipeline that builds, tests, and deploys everything in the right order.
- Dependency Management: Shared dependencies are deduped. Version conflicts are caught immediately.
Monorepo Structure
# Typical monorepo structure
my-monorepo/
apps/
web/ # Next.js frontend
package.json
src/
api/ # Express/Fastify backend
package.json
src/
admin/ # Admin dashboard
package.json
src/
packages/
ui/ # Shared React component library
package.json
src/
shared/ # Shared utilities and types
package.json
src/
config-eslint/ # Shared ESLint config
package.json
config-typescript/ # Shared tsconfig
package.json
package.json # Root workspace config
turbo.json # Turborepo config
pnpm-workspace.yaml # pnpm workspace config
Turborepo
Turborepo is a high-performance build system for JavaScript and TypeScript monorepos. It provides intelligent caching (local and remote), parallel task execution, and dependency-aware task ordering. It is framework-agnostic and works with npm, yarn, or pnpm.
# Create a new Turborepo project
npx create-turbo@latest my-monorepo
# Or add Turborepo to an existing monorepo
npm install turbo --save-dev
// turbo.json - Turborepo configuration
{
"$schema": "https://turbo.build/schema.json",
"globalDependencies": ["**/.env.*local"],
"tasks": {
"build": {
"dependsOn": ["^build"],
"inputs": ["$TURBO_DEFAULT$", ".env*"],
"outputs": [".next/**", "!.next/cache/**", "dist/**"]
},
"lint": {
"dependsOn": ["^build"]
},
"typecheck": {
"dependsOn": ["^build"]
},
"test": {
"dependsOn": ["^build"],
"inputs": ["src/**", "test/**"]
},
"dev": {
"cache": false,
"persistent": true
}
}
}
# Turborepo commands
npx turbo build # Build all packages in dependency order
npx turbo build --filter=web # Build only the web app and its dependencies
npx turbo build --filter=web... # Build web and everything it depends on
npx turbo build --filter=./apps/* # Build all apps
npx turbo dev # Start dev servers for all packages
npx turbo lint test --parallel # Run lint and test in parallel
npx turbo build --dry-run # Show what would run without executing
npx turbo build --graph # Generate a task dependency graph
pnpm Workspaces
pnpm is the preferred package manager for monorepos. It uses a content-addressable store that deduplicates packages across projects, saving significant disk space and installation time.
# pnpm-workspace.yaml
packages:
- "apps/*"
- "packages/*"
# pnpm workspace commands
pnpm install # Install all workspace dependencies
pnpm add zod --filter @myorg/shared # Add dep to specific package
pnpm add -D vitest --filter web # Add dev dep to web app
pnpm run build --filter web # Run build in web package
pnpm run build --filter "...web" # Build web and all its deps
pnpm run test -r # Run test in all packages recursively
pnpm --filter "./packages/**" build # Build all packages
Nx
Nx is a more full-featured build system compared to Turborepo. It includes code generators, dependency graph visualization, affected command analysis, and plugins for specific frameworks (React, Next.js, Angular, Node.js).
# Create a new Nx workspace
npx create-nx-workspace@latest my-org
# Add Nx to an existing monorepo
npx nx@latest init
# Nx commands
npx nx build web # Build the web app
npx nx affected --target=build # Build only affected projects
npx nx affected --target=test # Test only affected projects
npx nx graph # Open interactive dependency graph
npx nx run-many -t build test lint # Run multiple targets
npx nx generate @nx/react:component # Generate code with schematics
Sharing Code Between Packages
// packages/shared/package.json
{
"name": "@myorg/shared",
"version": "0.0.0",
"private": true,
"exports": {
".": "./src/index.ts",
"./types": "./src/types.ts",
"./utils": "./src/utils.ts"
}
}
// packages/ui/package.json
{
"name": "@myorg/ui",
"version": "0.0.0",
"private": true,
"exports": {
".": "./src/index.tsx",
"./button": "./src/button.tsx",
"./card": "./src/card.tsx"
},
"dependencies": {
"@myorg/shared": "workspace:*"
}
}
// apps/web/package.json
{
"name": "web",
"dependencies": {
"@myorg/shared": "workspace:*",
"@myorg/ui": "workspace:*"
}
}
// apps/web/src/app/page.tsx - Using shared packages
import { Button } from '@myorg/ui/button';
import { formatDate } from '@myorg/shared/utils';
import type { User } from '@myorg/shared/types';
Monorepo Best Practices
- Keep packages small and focused: Each package should have a clear, single responsibility.
- Use internal packages for shared code: Do not duplicate code between apps. Extract shared logic into packages.
- Share configurations: Create config packages for ESLint, TypeScript, and Prettier settings.
- Use remote caching in CI: Turborepo and Nx both support remote caching to avoid rebuilding unchanged packages.
- Choose pnpm for monorepos: Its strict dependency resolution and workspace protocol prevent phantom dependencies.