Back to blog

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 config

Workspace 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

🚀 Access Complete Guide

"Excellent material for those who want to go deeper!" - John, Developer

Comments (0)

This article has no comments yet 😢. Be the first! 🚀🦅

Add comments