Monorepo Guide 2026: Setting Up Turborepo and Nx for Modern JavaScript Projects
Monorepos โ single repositories containing multiple related packages or applications โ have become the standard architecture for medium and large JavaScript projects. Companies like Google, Meta, Vercel, and Microsoft manage enormous codebases in monorepos. The tooling in 2026 has matured dramatically: Turborepo and Nx are the two leading solutions, each with a distinct philosophy. This guide covers both tools, helps you choose between them, and provides practical setup instructions for real-world project structures.
Why Use a Monorepo?
Before picking a tool, understand the benefits and trade-offs that make monorepos compelling.
- Atomic cross-package changes โ Change a shared utility and the consuming apps in one commit, with one PR
- Shared tooling โ One ESLint config, one TypeScript base config, one testing setup
- Simplified dependency management โ No version mismatch between internal packages
- Faster CI with caching โ Only rebuild and retest what actually changed
- Code sharing without publishing โ Share code between apps without npm publish cycles
- Unified developer experience โ One place to clone, one
npm install, consistent commands
| Feature | Turborepo | Nx |
|---|---|---|
| Learning curve | Low | Medium-High |
| Config overhead | Minimal | Moderate |
| Build caching | Excellent (local + remote) | Excellent (local + remote) |
| Task orchestration | Pipeline-based | Graph-based (more powerful) |
| Code generation | Basic generators | Full generator system |
| IDE integration | Good | Excellent (Nx Console) |
| Best for | Frontend/fullstack, Next.js shops | Enterprise, Angular, mixed stacks |
Turborepo Setup
Turborepo (from Vercel) is the simpler of the two. It focuses on task running and caching with minimal configuration. It integrates naturally with npm/yarn/pnpm workspaces.
Initial Setup
# Create a new Turborepo project
npx create-turbo@latest my-monorepo
# Or add Turborepo to an existing workspace
npm install turbo --save-dev --workspace-root
# Recommended: use pnpm for best workspace performance
pnpm dlx create-turbo@latest my-monorepo --package-manager pnpmProject Structure
my-monorepo/
โโโ apps/
โ โโโ web/ # Next.js app
โ โ โโโ package.json
โ โ โโโ ...
โ โโโ admin/ # Another Next.js app
โ โ โโโ package.json
โ โ โโโ ...
โ โโโ api/ # Express/Fastify API
โ โโโ package.json
โ โโโ ...
โโโ packages/
โ โโโ ui/ # Shared React component library
โ โ โโโ package.json # name: "@acme/ui"
โ โ โโโ src/
โ โ โ โโโ Button.tsx
โ โ โ โโโ Card.tsx
โ โ โ โโโ index.ts
โ โ โโโ tsconfig.json
โ โโโ config/ # Shared configs
โ โ โโโ package.json # name: "@acme/config"
โ โ โโโ eslint/
โ โ โ โโโ index.js
โ โ โโโ typescript/
โ โ โโโ base.json
โ โ โโโ nextjs.json
โ โ โโโ react-library.json
โ โโโ utils/ # Shared utilities
โ โโโ package.json # name: "@acme/utils"
โ โโโ src/
โ โโโ format.ts
โ โโโ validate.ts
โโโ turbo.json # Turborepo config
โโโ package.json # Root workspace config
โโโ pnpm-workspace.yamlturbo.json Configuration
{
"$schema": "https://turbo.build/schema.json",
"globalDependencies": ["**/.env.*local"],
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": [".next/**", "!.next/cache/**", "dist/**"],
"cache": true
},
"dev": {
"dependsOn": ["^build"],
"cache": false,
"persistent": true
},
"test": {
"dependsOn": ["build"],
"outputs": ["coverage/**"],
"inputs": ["src/**/*.tsx", "src/**/*.ts", "test/**/*.ts"]
},
"lint": {
"outputs": [],
"inputs": ["src/**/*.tsx", "src/**/*.ts", "*.json", ".eslintrc.*"]
},
"type-check": {
"dependsOn": ["^build"],
"outputs": []
},
"clean": {
"cache": false
}
}
}The ^build syntax means "run build in all dependencies first." This ensures @acme/ui is built before the apps that depend on it.
Root package.json
{
"name": "my-monorepo",
"private": true,
"scripts": {
"build": "turbo run build",
"dev": "turbo run dev",
"test": "turbo run test",
"lint": "turbo run lint",
"type-check": "turbo run type-check",
"clean": "turbo run clean && rm -rf node_modules",
"format": "prettier --write "**/*.{ts,tsx,md,json}" --ignore-path .gitignore"
},
"devDependencies": {
"turbo": "latest",
"prettier": "^3.0.0",
"typescript": "^5.4.0"
},
"packageManager": "pnpm@9.0.0"
}pnpm-workspace.yaml
packages:
- 'apps/*'
- 'packages/*'Shared TypeScript Configuration
// packages/config/typescript/base.json
{
"$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022"],
"module": "ESNext",
"moduleResolution": "bundler",
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"exactOptionalPropertyTypes": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
}
}
// packages/config/typescript/nextjs.json
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "./base.json",
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "ES2022"],
"allowJs": true,
"jsx": "preserve",
"incremental": true,
"plugins": [{ "name": "next" }],
"paths": { "@/*": ["./src/*"] }
}
}Internal Package Setup
// packages/ui/package.json
{
"name": "@acme/ui",
"version": "0.0.1",
"private": true,
"main": "./src/index.ts",
"types": "./src/index.ts",
"exports": {
".": {
"types": "./src/index.ts",
"default": "./src/index.ts"
}
},
"scripts": {
"lint": "eslint src/ --ext .ts,.tsx",
"type-check": "tsc --noEmit"
},
"devDependencies": {
"@acme/config": "workspace:*",
"@types/react": "^18.3.0",
"typescript": "^5.4.0"
},
"peerDependencies": {
"react": "^18.0.0 || ^19.0.0"
}
}// packages/ui/src/Button.tsx
import { type ButtonHTMLAttributes } from 'react';
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'secondary' | 'danger';
size?: 'sm' | 'md' | 'lg';
isLoading?: boolean;
}
export function Button({
variant = 'primary',
size = 'md',
isLoading = false,
children,
disabled,
className = '',
...props
}: ButtonProps) {
const variantClasses = {
primary: 'bg-blue-600 text-white hover:bg-blue-700',
secondary: 'bg-gray-100 text-gray-900 hover:bg-gray-200',
danger: 'bg-red-600 text-white hover:bg-red-700',
};
const sizeClasses = {
sm: 'px-3 py-1.5 text-sm',
md: 'px-4 py-2 text-base',
lg: 'px-6 py-3 text-lg',
};
return (
<button
{...props}
disabled={disabled || isLoading}
className={`${variantClasses[variant]} ${sizeClasses[size]} rounded-lg font-medium transition-colors ${className}`}
>
{isLoading ? 'Loading...' : children}
</button>
);
}
// packages/ui/src/index.ts โ barrel export
export { Button } from './Button';
export { Card } from './Card';
export type { ButtonProps } from './Button';Remote Caching
Remote caching is one of Turborepo's most powerful features. Build artifacts are shared across all developers and CI machines, so if a colleague already built the same commit, you get an instant cache hit instead of rebuilding.
# Vercel Remote Cache (free for Vercel users)
npx turbo login
npx turbo link
# Self-hosted remote cache (Turborepo supports any S3-compatible store)
# Set these environment variables:
# TURBO_API=https://your-cache-server.com
# TURBO_TOKEN=your-token
# TURBO_TEAM=your-team-slug
# GitHub Actions with remote cache
# .github/workflows/ci.yml
# env:
# TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
# TURBO_TEAM: ${{ vars.TURBO_TEAM }}Nx Setup
Nx provides a more structured and feature-rich monorepo experience. It shines in large teams, enterprise environments, and projects with multiple frameworks (React, Angular, Node.js, etc.).
# Create new Nx workspace
npx create-nx-workspace@latest my-workspace
# Add to existing project
npx nx@latest init
# Add Next.js app
nx generate @nx/next:application web --directory=apps/web
# Add a shared library
nx generate @nx/react:library ui --directory=packages/ui --bundler=vite
# Add an Express API
nx generate @nx/express:application api --directory=apps/api
# Run affected tasks only (most powerful Nx feature)
# Only tests packages changed since last commit
nx affected --target=test
# Visualize the project dependency graph
nx graphnx.json Configuration
{
"$schema": "./node_modules/nx/schemas/nx-schema.json",
"defaultBase": "main",
"namedInputs": {
"default": ["{projectRoot}/**/*", "sharedGlobals"],
"production": [
"default",
"!{projectRoot}/**/?(*.)+(spec|test).[jt]s?(x)?(.snap)",
"!{projectRoot}/tsconfig.spec.json",
"!{projectRoot}/.eslintrc.json"
],
"sharedGlobals": ["{workspaceRoot}/.github/workflows/ci.yml"]
},
"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
}
},
"plugins": [
"@nx/next/plugin",
"@nx/eslint/plugin",
"@nx/vite/plugin"
]
}CI/CD Pipeline
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # Required for Nx affected commands
- uses: pnpm/action-setup@v3
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: 20
cache: pnpm
- name: Install dependencies
run: pnpm install --frozen-lockfile
# For Turborepo
- name: Build
run: pnpm turbo build --filter="...[origin/main]"
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
- name: Test
run: pnpm turbo test --filter="...[origin/main]"
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
# For Nx (alternative)
# - name: Build affected
# run: npx nx affected --target=build --base=origin/main
# - name: Test affected
# run: npx nx affected --target=test --base=origin/mainChoosing: Turborepo vs Nx
- Choose Turborepo if you want minimal config, are already using Vercel, your stack is primarily Next.js, or you want to get started in under an hour.
- Choose Nx if you need code generation, have a large team that benefits from enforced project boundaries, work with multiple frameworks, or need the project graph visualization to understand dependency relationships.
- Both are excellent choices for remote caching and affected task running, which are the core features that make monorepos fast in CI.
When working with your monorepo packages, our JSON Formatter tool can help validate package.json and turbo.json files. For generating unique IDs across your packages, try our UUID Generator. You can also read our Git Branching Strategies guide for branch management patterns that work well with monorepo workflows.