JavaScript Monorepos: Complete Guide with Turborepo and Nx in 2025
Hello HaWkers, monorepos have become the default choice for teams managing multiple related packages or applications. Companies like Google, Meta, Microsoft, and Vercel use monorepos at massive scale.
Have you ever had to maintain multiple repositories synchronized, with different dependency versions and manual release processes? Monorepos solve these problems elegantly.
What is a Monorepo and Why Use It
A monorepo is a single repository containing multiple distinct but related projects. It's not the same as a "monolith" - each project can be independent.
Advantages of Monorepos
Simplified code sharing:
- Internal packages are imported directly
- Changes propagate instantly
- No need to publish to npm for internal use
Atomic commits:
- Changes affecting multiple packages in a single commit
- Coordinated refactoring
- Unified change history
Unified tooling:
- One ESLint, Prettier, TypeScript configuration
- Centralized CI/CD
- Deduplicated dependencies
Disadvantages to consider:
- Initial learning curve
- Repository can get large
- Requires specialized tools to scale
Basic Structure of a Monorepo
Directory Structure
my-monorepo/
├── apps/ # Applications
│ ├── web/ # Next.js App
│ │ ├── src/
│ │ ├── package.json
│ │ └── tsconfig.json
│ ├── mobile/ # React Native App
│ │ ├── src/
│ │ └── package.json
│ └── api/ # Node.js API
│ ├── src/
│ └── package.json
│
├── packages/ # Shared packages
│ ├── ui/ # UI Components
│ │ ├── src/
│ │ └── package.json
│ ├── utils/ # Utility functions
│ │ ├── src/
│ │ └── package.json
│ ├── config-eslint/ # ESLint Configuration
│ │ └── package.json
│ └── config-typescript/ # TypeScript Configuration
│ └── package.json
│
├── package.json # Root package.json
├── pnpm-workspace.yaml # Workspace config (pnpm)
├── turbo.json # Turborepo config
└── tsconfig.json # Base TypeScript configWorkspace Configuration (pnpm)
# pnpm-workspace.yaml
packages:
- 'apps/*'
- 'packages/*'// package.json (root)
{
"name": "my-monorepo",
"private": true,
"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"
}
Setting Up Turborepo
Turborepo is the most popular tool for managing JavaScript monorepos in 2025, acquired by Vercel.
Basic Configuration
// turbo.json
{
"$schema": "https://turbo.build/schema.json",
"globalDependencies": ["**/.env.*local"],
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", ".next/**", "!.next/cache/**"]
},
"lint": {
"dependsOn": ["^build"]
},
"test": {
"dependsOn": ["build"],
"inputs": ["src/**/*.tsx", "src/**/*.ts", "test/**/*.ts"]
},
"dev": {
"cache": false,
"persistent": true
},
"clean": {
"cache": false
}
}
}Pipeline with Remote Cache
// turbo.json - Advanced configuration
{
"$schema": "https://turbo.build/schema.json",
"globalDependencies": [
".env",
"**/.env.*local"
],
"globalEnv": [
"NODE_ENV",
"VERCEL_URL"
],
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", ".next/**", "!.next/cache/**"],
"env": ["DATABASE_URL", "API_KEY"]
},
"build:docker": {
"dependsOn": ["build"],
"outputs": ["Dockerfile.built"]
},
"test": {
"dependsOn": ["build"],
"outputs": ["coverage/**"],
"inputs": [
"src/**/*.tsx",
"src/**/*.ts",
"**/*.test.ts",
"**/*.test.tsx"
]
},
"test:e2e": {
"dependsOn": ["build"],
"outputs": ["playwright-report/**"],
"inputs": ["e2e/**/*.ts"]
},
"lint": {
"outputs": [],
"inputs": ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"]
},
"typecheck": {
"dependsOn": ["^build"],
"outputs": []
},
"dev": {
"cache": false,
"persistent": true
}
}
}
Shared Packages
UI Components Package
// packages/ui/package.json
{
"name": "@myorg/ui",
"version": "0.0.0",
"private": true,
"main": "./dist/index.js",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.mjs",
"require": "./dist/index.js"
},
"./button": {
"types": "./dist/button.d.ts",
"import": "./dist/button.mjs",
"require": "./dist/button.js"
}
},
"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/config-typescript": "workspace:*",
"tsup": "^8.0.0",
"typescript": "^5.3.0"
},
"peerDependencies": {
"react": "^18.0.0"
}
}// packages/ui/src/index.ts
export { Button, type ButtonProps } from './button';
export { Input, type InputProps } from './input';
export { Card, type CardProps } from './card';
export { Modal, type ModalProps } from './modal';// packages/ui/src/button.tsx
import { forwardRef, ButtonHTMLAttributes } from 'react';
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'secondary' | 'outline' | 'ghost';
size?: 'sm' | 'md' | 'lg';
isLoading?: boolean;
}
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
({ variant = 'primary', size = 'md', isLoading, children, className, disabled, ...props }, ref) => {
const baseStyles = 'inline-flex items-center justify-center font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed';
const variants = {
primary: 'bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500',
secondary: 'bg-gray-600 text-white hover:bg-gray-700 focus:ring-gray-500',
outline: 'border-2 border-gray-300 bg-transparent hover:bg-gray-100 focus:ring-gray-500',
ghost: 'bg-transparent hover:bg-gray-100 focus:ring-gray-500',
};
const sizes = {
sm: 'px-3 py-1.5 text-sm rounded',
md: 'px-4 py-2 text-base rounded-md',
lg: 'px-6 py-3 text-lg rounded-lg',
};
return (
<button
ref={ref}
className={`${baseStyles} ${variants[variant]} ${sizes[size]} ${className || ''}`}
disabled={disabled || isLoading}
{...props}
>
{isLoading ? (
<>
<svg className="animate-spin -ml-1 mr-2 h-4 w-4" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
</svg>
Loading...
</>
) : (
children
)}
</button>
);
}
);
Button.displayName = 'Button';
Utilities Package
// packages/utils/src/index.ts
export * from './string';
export * from './date';
export * from './validation';
export * from './formatting';// packages/utils/src/validation.ts
export const validators = {
email: (value: string): boolean => {
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return regex.test(value);
},
ssn: (value: string): boolean => {
const ssn = value.replace(/\D/g, '');
if (ssn.length !== 9) return false;
// Basic SSN validation
return /^\d{9}$/.test(ssn);
},
phone: (value: string): boolean => {
const phone = value.replace(/\D/g, '');
return phone.length >= 10 && phone.length <= 11;
},
url: (value: string): boolean => {
try {
new URL(value);
return true;
} catch {
return false;
}
},
};
export function createValidator<T extends Record<string, unknown>>(
rules: Record<keyof T, (value: unknown) => string | null>
) {
return (data: T): Record<keyof T, string | null> => {
const errors = {} as Record<keyof T, string | null>;
for (const [field, validator] of Object.entries(rules)) {
errors[field as keyof T] = validator(data[field as keyof T]);
}
return errors;
};
}
Using Nx as an Alternative
Nx is more opinionated than Turborepo and offers more built-in tools.
Nx Setup
npx create-nx-workspace@latest my-workspace --preset=ts// nx.json
{
"$schema": "./node_modules/nx/schemas/nx-schema.json",
"targetDefaults": {
"build": {
"dependsOn": ["^build"],
"inputs": ["production", "^production"],
"cache": true
},
"test": {
"inputs": ["default", "^production"],
"cache": true
},
"lint": {
"inputs": ["default", "{workspaceRoot}/.eslintrc.json"],
"cache": true
}
},
"namedInputs": {
"default": ["{projectRoot}/**/*", "sharedGlobals"],
"production": [
"default",
"!{projectRoot}/**/*.spec.ts",
"!{projectRoot}/**/*.test.ts"
],
"sharedGlobals": ["{workspaceRoot}/tsconfig.base.json"]
},
"plugins": [
"@nx/vite/plugin",
"@nx/eslint/plugin"
]
}Nx Generators
# Create new library
npx nx generate @nx/js:library my-lib --directory=packages/my-lib
# Create new React app
npx nx generate @nx/react:application my-app --directory=apps/my-app
# Visualize dependencies
npx nx graph
Shared Configurations
Shared ESLint
// packages/config-eslint/index.js
module.exports = {
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react/recommended',
'plugin:react-hooks/recommended',
'prettier',
],
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint', 'react', 'react-hooks'],
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
ecmaFeatures: {
jsx: true,
},
},
settings: {
react: {
version: 'detect',
},
},
rules: {
'react/react-in-jsx-scope': 'off',
'react/prop-types': 'off',
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/no-explicit-any': 'warn',
},
ignorePatterns: ['dist', 'node_modules', '.turbo'],
};// apps/web/.eslintrc.js
module.exports = {
root: true,
extends: ['@myorg/config-eslint'],
parserOptions: {
project: './tsconfig.json',
tsconfigRootDir: __dirname,
},
};Shared TypeScript
// packages/config-typescript/base.json
{
"$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": {
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"declaration": true,
"declarationMap": true,
"inlineSources": false,
"preserveWatchOutput": true
},
"exclude": ["node_modules"]
}// packages/config-typescript/react.json
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "./base.json",
"compilerOptions": {
"lib": ["DOM", "DOM.Iterable", "ES2022"],
"jsx": "react-jsx",
"module": "ESNext",
"target": "ES2022"
}
}
CI/CD for Monorepos
Optimized GitHub Actions
# .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
- 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
- name: Build affected packages
run: pnpm turbo run build --filter=...[HEAD^1]
- name: Test affected packages
run: pnpm turbo run test --filter=...[HEAD^1]
- name: Lint affected packages
run: pnpm turbo run lint --filter=...[HEAD^1]
deploy:
needs: build
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v2
with:
version: 8
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
- name: Build all
run: pnpm turbo run build
- name: Deploy web
run: pnpm --filter=@myorg/web deploy
env:
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}Conclusion
Monorepos offer significant benefits for teams managing multiple related packages. Modern tools like Turborepo and Nx have made monorepo management accessible even for smaller teams.
When to use monorepo:
- Multiple packages with shared code
- Teams working on cross-cutting features
- Need for coordinated releases
When to avoid:
- Completely independent projects
- Teams with low communication
- Single very small repository
If you want to deepen your knowledge in JavaScript architecture, I recommend checking out another article: Vite vs Webpack in 2025 where you'll discover how to choose the ideal build tool for your packages.
Let's go! 🦅
🎯 Join Developers Who Are Evolving
Thousands of developers already use our material to accelerate their studies and achieve better positions in the market.
Why invest in structured knowledge?
Learning in an organized way with practical examples makes all the difference in your journey as a developer.
Start now:
- 1x of $4.90 on card
- or $4.90 at sight
"Excellent material for those who want to go deeper!" - John, Developer

