# Setting Up a Modern Monorepo with NX

## Introduction

Managing multiple related projects can quickly become overwhelming when scattered across different repositories. NX offers a powerful solution for organizing React applications, shared libraries, and backend services in a single, well-structured monorepo. This guide walks you through setting up a production-ready NX workspace with modern best practices.

## Why Choose NX for Your Monorepo?

NX has evolved significantly and now offers compelling advantages for development teams:

1. **Intelligent Caching**: NX automatically caches task results locally and remotely, dramatically reducing build and test times
    
2. **Smart Builds**: Only rebuilds projects affected by your changes, saving significant CI/CD time
    
3. **Dependency Visualization**: Interactive dependency graphs help you understand your codebase architecture
    
4. **Modern Tooling**: Built-in support for Jest, Vitest, Cypress, Playwright, and Storybook
    
5. **Framework Agnostic**: Supports React, Angular, Vue, Node.js, and many other frameworks
    
6. **Code Generation**: Powerful generators create consistent, well-structured code across your workspace
    
7. **Nx Cloud Integration**: Optional remote caching and distributed task execution for enterprise teams
    

## Getting Started

### 1\. Choose Your Monorepo Strategy

NX offers two approaches for organizing monorepos: **Integrated** and **Package-Based**.

**Integrated Approach (Recommended for Most Teams)**:

* All dependencies in root `package.json`
    
* Single version policy across the workspace
    
* Faster setup for new projects
    
* Easier dependency management
    
* Better for teams working on interconnected projects
    

**Package-Based Approach**:

* More suitable if you need different dependencies and tooling between applications
    
* Each project can have its own `package.json` with specific versions
    
* Better for migrating existing projects
    
* More flexibility but increased complexity
    
* Ideal for independent apps with different React versions or tech stacks
    

### 2\. Create Your NX Workspace

The latest version of NX (v21+) has streamlined workspace creation:

```bash
npx create-nx-workspace@latest my-workspace
```

During setup, you'll be prompted to choose:

* **Preset**: Select `react-monorepo` for a React-focused workspace
    
* **Bundler**: Choose `vite` (recommended) or `webpack`
    
* **Test Runner**: Select `vitest` (modern) or `jest`
    
* **Nx Cloud**: Enable for remote caching (recommended for teams)
    

For an **integrated workspace** (default), your structure will look like:

```javascript
my-workspace/
├── apps/              # Applications live here
├── libs/              # Shared libraries
├── nx.json            # NX configuration
├── package.json       # Single source of truth for dependencies
└── tsconfig.base.json # Path mappings for imports
```

For a **package-based workspace**, each app/lib will have its own `package.json`.

### 2\. Generate a React Application

The `@nx/react` plugin (formerly `@nrwl/react`) provides updated generators:

```bash
# Install the React plugin (if not already installed)
npm install --save-dev @nx/react

# Generate a React application
npx nx generate @nx/react:application my-app --style=scss --bundler=vite
```

### 3\. Managing Multiple React Apps with Different Versions

One of the challenges in a monorepo is managing multiple apps that may need different versions of dependencies. Here's how to handle this:

#### Option 1: Using NPM Aliases (Integrated Approach)

If you need to support different versions of React in the same workspace, you can use NPM aliases:

```json
// Root package.json
{
  "dependencies": {
    "react": "^19.2.0",
    "react-dom": "^19.2.0",
    "react-18": "npm:react@^18.2.0",
    "react-dom-18": "npm:react-dom@^18.2.0"
  }
}
```

Then configure your legacy app's `tsconfig.json` to map to the older version:

```json
// apps/legacy-app/tsconfig.json
{
  "compilerOptions": {
    "paths": {
      "react": ["../../node_modules/react-18"],
      "react-dom": ["../../node_modules/react-dom-18"]
    }
  }
}
```

#### Option 2: Package-Based Workspace (Recommended for Different Versions)

For scenarios where apps truly need different dependency versions, use a package-based repository which is more suitable for having different dependencies and tooling between applications:

```bash
# Create a package-based workspace
npx create-nx-workspace@latest my-workspace --preset=npm
```

In this setup, each application has its own `package.json`:

```javascript
my-workspace/
├── packages/
│   ├── modern-app/
│   │   ├── package.json      # React 19
│   │   └── src/
│   └── legacy-app/
│       ├── package.json      # React 18
│       └── src/
└── package.json              # Root for shared dev dependencies
```

**Modern App** (`packages/modern-app/package.json`):

```json
{
  "name": "@my-workspace/modern-app",
  "version": "1.0.0",
  "dependencies": {
    "react": "^19.2.0",
    "react-dom": "^19.2.0",
    "@my-workspace/shared-ui-v3": "*"
  }
}
```

**Legacy App** (`packages/legacy-app/package.json`):

```json
{
  "name": "@my-workspace/legacy-app",
  "version": "2.5.0",
  "dependencies": {
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "lodash": "^4.17.0",
    "@my-workspace/shared-ui-legacy": "*"
  }
}
```

#### Option 3: Per-Project Dependency Overrides (Integrated Approach)

Even with an integrated approach, you can enable project-level dependency overrides. Create a `package.json` in your specific app:

```json
// apps/legacy-app/package.json
{
  "name": "legacy-app",
  "dependencies": {
    "react": "18.2.0",
    "react-dom": "18.2.0"
  }
}
```

Then configure your package manager (pnpm example) to use these overrides:

```yaml
# pnpm-workspace.yaml
packages:
  - 'apps/*'
  - 'libs/*'
```

**Important Considerations:**

* If projects use different versions of React, runtime issues can occur when sharing components between projects
    
* Maintain separate component libraries for each major version
    
* Use caution when sharing code between apps with different dependency versions
    
* React 19 introduced major features like Server Components and the Actions API, which may not be compatible with older React versions
    

### 3\. Create Shared Libraries

Libraries enable code sharing across applications:

```bash
# Generate a UI component library
npx nx generate @nx/react:library ui-components --directory=libs/shared --style=scss --bundler=vite

# Generate a utilities library
npx nx generate @nx/react:library utils --directory=libs/shared --bundler=none
```

**Key Options:**

* `--directory`: Organizes libraries into folders
    
* `--style`: CSS/SCSS/styled-components/emotion
    
* `--bundler`: `vite`, `rollup`, or `none` (for non-buildable libraries)
    
* `--unitTestRunner`: `vitest`, `jest`, or `none`
    

**Creating Version-Specific Libraries:**

When managing apps with different React versions, create separate libraries for each version:

```bash
# Modern UI library (React 19)
npx nx generate @nx/react:library ui-modern --directory=libs/shared --style=scss

# Legacy UI library (React 18)
npx nx generate @nx/react:library ui-legacy --directory=libs/shared --style=scss
```

Configure library dependencies in `package.json` (package-based) or use peer dependencies:

```json
// libs/shared/ui-modern/package.json
{
  "peerDependencies": {
    "react": "^19.0.0",
    "react-dom": "^19.0.0"
  }
}

// libs/shared/ui-legacy/package.json
{
  "peerDependencies": {
    "react": "^18.0.0",
    "react-dom": "^18.0.0"
  }
}
```

### 4\. Generate Components

Create components within your libraries:

```bash
# Generate a component in a library
npx nx generate @nx/react:component Button --project=ui-components --directory=src/lib

# Generate a component with export
npx nx generate @nx/react:component Header --project=ui-components --export
```

The component will be generated with:

* Component file (`.tsx`)
    
* Style file (`.scss`)
    
* Test file (`.spec.tsx`)
    
* Automatic export from library's index file (if `--export` is used)
    

## Working with State Management

### Redux Toolkit

NX provides a Redux slice generator:

```bash
npx nx generate @nx/react:redux todos --project=my-app
```

This generates:

* Redux slice with TypeScript
    
* Configured store setup
    
* Test files
    

For shared state across apps, create a dedicated state library:

```bash
npx nx generate @nx/react:library state --directory=libs/shared
npx nx generate @nx/react:redux users --project=state
```

## Sharing Assets and Styles

### Global Styles Library

Create a centralized styles library:

```bash
npx nx generate @nx/react:library styles --directory=libs/shared --style=scss --bundler=none
```

Configure in your app's `project.json`:

```json
{
  "targets": {
    "build": {
      "options": {
        "styles": ["libs/shared/styles/src/lib/global.scss"],
        "stylePreprocessorOptions": {
          "includePaths": ["libs/shared/styles/src/lib"]
        }
      }
    }
  }
}
```

Then import in your components:

```scss
@import "variables";
@import "mixins";

.my-component {
  background: $primary-color;
}
```

### Shared Assets

Configure assets in `project.json`:

```json
{
  "targets": {
    "build": {
      "options": {
        "assets": [
          "apps/my-app/src/favicon.ico",
          "apps/my-app/src/assets",
          {
            "input": "libs/shared/assets/src",
            "glob": "**/*",
            "output": "assets"
          }
        ]
      }
    }
  }
}
```

## Testing Configuration

### Unit Testing with Vitest (Recommended)

Modern NX workspaces use Vitest by default:

```bash
# Run tests for a specific project
npx nx test my-app

# Run tests for all projects
npx nx run-many --target=test

# Run tests only for affected projects
npx nx affected --target=test
```

Configure coverage in `vitest.config.ts`:

```javascript
import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    coverage: {
      provider: 'v8',
      reporter: ['text', 'html', 'lcov'],
      thresholds: {
        branches: 80,
        functions: 80,
        lines: 80,
        statements: 80
      }
    }
  }
});
```

### E2E Testing with Cypress

Generate E2E tests with Cypress:

```bash
# Configure Cypress for an application
npx nx generate @nx/cypress:configuration --project=my-app

# Run Cypress tests
npx nx e2e my-app-e2e

# Run Cypress in interactive mode
npx nx e2e my-app-e2e --watch
```

Cypress tests are automatically generated in a separate e2e project:

```javascript
my-workspace/
├── apps/
│   ├── my-app/
│   └── my-app-e2e/              # Cypress E2E tests
│       ├── src/
│       │   ├── e2e/
│       │   │   └── app.cy.ts
│       │   ├── fixtures/
│       │   └── support/
│       ├── cypress.config.ts
│       └── project.json
```

## Storybook Integration

Set up Storybook for component development:

```bash
# Configure Storybook for a library
npx nx generate @nx/react:storybook-configuration ui-components

# Run Storybook
npx nx storybook ui-components

# Generate stories for all components
npx nx generate @nx/react:stories ui-components
```

**Modern Storybook Setup (v7+):**

* Uses Vite for faster builds
    
* Component Story Format 3 (CSF3)
    
* Automatic story generation
    
* Interaction testing support
    

## Building and Deployment

### Production Build

```bash
# Build a specific app
npx nx build my-app --configuration=production

# Build all apps
npx nx run-many --target=build --configuration=production

# Build only affected projects
npx nx affected --target=build --configuration=production
```

### Build Optimization

Configure in `project.json`:

```json
{
  "targets": {
    "build": {
      "configurations": {
        "production": {
          "optimization": true,
          "extractCss": true,
          "sourceMap": false,
          "namedChunks": false
        }
      }
    }
  }
}
```

## Essential NX Commands

### Workspace Operations

```bash
# View project dependency graph
npx nx graph

# List all projects
npx nx show projects

# Show project details
npx nx show project my-app

# Run linting
npx nx lint my-app

# Format code
npx nx format:write
```

### Affected Commands

NX's killer feature - only work on what changed:

```bash
# Show affected projects
npx nx affected:graph

# Test affected projects
npx nx affected --target=test

# Build affected projects
npx nx affected --target=build

# Lint affected projects
npx nx affected --target=lint
```

### Removing Projects

```bash
# Remove a project
npx nx generate @nx/workspace:remove my-old-app

# Dry run first
npx nx generate @nx/workspace:remove my-old-app --dry-run
```

## Path Mapping and Imports

NX configures TypeScript path mappings in `tsconfig.base.json`:

```json
{
  "compilerOptions": {
    "paths": {
      "@my-workspace/ui-components": ["libs/shared/ui-components/src/index.ts"],
      "@my-workspace/utils": ["libs/shared/utils/src/index.ts"],
      "@my-workspace/state": ["libs/shared/state/src/index.ts"]
    }
  }
}
```

Import like this:

```javascript
import { Button } from '@my-workspace/ui-components';
import { formatDate } from '@my-workspace/utils';
```

## CI/CD Integration

NX automatically generates GitHub Actions configuration:

```yaml
# .github/workflows/ci.yml
name: CI
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  main:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'
      
      - run: npm ci
      
      - uses: nrwl/nx-set-shas@v4
      
      - run: npx nx affected --target=lint
      - run: npx nx affected --target=test
      - run: npx nx affected --target=build
```

## Performance Best Practices

1. **Enable Nx Cloud**: Provides remote caching and distributed task execution
    
2. **Use Affected Commands**: Only build/test what changed
    
3. **Configure Caching**: Ensure all targets are cacheable in `nx.json`
    
4. **Use Vite**: Much faster than webpack for development and builds
    
5. **Parallel Execution**: NX automatically runs tasks in parallel when possible
    
6. **Choose the Right Strategy**:
    
    * Use **integrated** for better performance and simpler dependency management
        
    * Use **package-based** only when you truly need different dependency versions across apps
        

## Dependency Management Strategies Comparison

NX fully supports both dependency management strategies, and you can even mix these strategies, using a single version policy for most dependencies while allowing specific projects to maintain their own versions when necessary.

### Single Version Policy (Integrated - Recommended)

**Pros:**

* ✅ Simpler dependency management
    
* ✅ Better caching and performance
    
* ✅ Reduces duplication and conflicts
    
* ✅ Easier upgrades (update once, affects all)
    
* ✅ Guaranteed compatibility between shared libraries
    

**Cons:**

* ❌ All apps must coordinate on dependency versions
    
* ❌ Breaking changes affect entire workspace
    
* ❌ May need to keep older apps on older framework versions
    

### Independent Dependencies (Package-Based)

**Pros:**

* ✅ Each app/package controls its own dependencies
    
* ✅ Easier to migrate existing projects
    
* ✅ Can run different React versions simultaneously
    
* ✅ Individual apps can upgrade independently
    

**Cons:**

* ❌ More complex dependency management
    
* ❌ Potential version conflicts when sharing code
    
* ❌ Larger total `node_modules` size
    
* ❌ Harder to ensure compatibility
    
* ❌ Runtime errors when sharing components between different React versions
    

### When to Use Each Approach

**Use Integrated (Single Version) When:**

* All apps are actively maintained
    
* Teams can coordinate on upgrades
    
* Apps share significant code
    
* Performance and caching are priorities
    
* Starting a new monorepo from scratch
    

**Use Package-Based (Multiple Versions) When:**

* Migrating existing independent projects
    
* Apps have vastly different technology needs
    
* You need to support legacy apps alongside modern ones
    
* Different teams own different apps with minimal overlap
    
* Apps are published as independent NPM packages
    

## Key Benefits Summary

* ✅ **Test Components in Isolation**: Storybook integration for component development
    
* ✅ **Easy Code Sharing**: Import libraries with clean TypeScript paths
    
* ✅ **Modern Tooling**: Vite, Vitest, Cypress, ESLint, Prettier pre-configured
    
* ✅ **CLI Generators**: Bootstrap apps, libraries, and components with consistent structure
    
* ✅ **Visual Dependency Graph**: Understand project relationships at a glance
    
* ✅ **Smart CI/CD**: Only test and build affected projects
    
* ✅ **Framework Flexibility**: React, Node.js, and many other frameworks in one repo
    
* ✅ **Type Safety**: Full TypeScript support across the workspace
    

## Migration Notes from Legacy NX

If updating from older NX versions:

1. **Package Names**: `@nrwl/*` → `@nx/*`
    
2. **Commands**: `nx affected:test` → `nx affected --target=test`
    
3. **Bundlers**: Consider migrating from webpack to Vite
    
4. **Test Runners**: Consider migrating from Jest to Vitest
    
5. **Configuration**: `workspace.json` deprecated, use `project.json` files
    

Run the migration command:

```bash
npx nx migrate latest
npx nx migrate --run-migrations
```

## Final Repository Structure

After following this guide, here are two example structures depending on your approach:

### Integrated Workspace Structure (Single Version Policy)

```javascript
my-workspace/
├── apps/
│   ├── customer-portal/                 # Modern React 19 app
│   │   ├── src/
│   │   │   ├── app/
│   │   │   │   ├── app.tsx
│   │   │   │   ├── app.module.scss
│   │   │   │   └── redux/
│   │   │   ├── main.tsx
│   │   │   ├── assets/
│   │   │   └── styles.scss
│   │   ├── project.json
│   │   ├── vite.config.ts
│   │   └── tsconfig.json
│   │
│   ├── customer-portal-e2e/
│   │   ├── src/
│   │   │   ├── e2e/
│   │   │   │   └── app.cy.ts
│   │   │   └── support/
│   │   ├── cypress.config.ts
│   │   └── project.json
│   │
│   └── admin-dashboard/                 # Another React 19 app
│       ├── src/
│       ├── project.json
│       └── vite.config.ts
│
├── libs/
│   └── shared/
│       ├── ui-components/              # Shared React components
│       │   ├── src/
│       │   │   ├── index.ts
│       │   │   └── lib/
│       │   │       ├── button/
│       │   │       ├── header/
│       │   │       └── card/
│       │   ├── .storybook/
│       │   ├── project.json
│       │   └── vite.config.ts
│       │
│       ├── utils/                      # Shared utilities
│       │   ├── src/
│       │   │   ├── index.ts
│       │   │   └── lib/
│       │   │       ├── date-utils.ts
│       │   │       └── string-utils.ts
│       │   └── project.json
│       │
│       ├── state/                      # Shared Redux state
│       │   ├── src/
│       │   │   ├── index.ts
│       │   │   └── lib/
│       │   │       ├── store.ts
│       │   │       └── slices/
│       │   └── project.json
│       │
│       ├── styles/                     # Shared SCSS
│       │   ├── src/
│       │   │   └── lib/
│       │   │       ├── _variables.scss
│       │   │       ├── _mixins.scss
│       │   │       └── global.scss
│       │   └── project.json
│       │
│       └── assets/                     # Shared images, icons
│           └── src/
│
├── .github/
│   └── workflows/
│       └── ci.yml
│
├── node_modules/
├── dist/
├── nx.json
├── package.json                        # Single source for all dependencies
├── tsconfig.base.json
└── README.md
```

### Package-Based Structure (Multiple Versions Support)

Best for scenarios where apps need different React versions or independent dependencies:

```javascript
my-workspace/
├── packages/
│   ├── modern-app/                     # React 19 application
│   │   ├── src/
│   │   │   ├── app/
│   │   │   └── main.tsx
│   │   ├── package.json                # React 19.2.0
│   │   ├── project.json
│   │   ├── vite.config.ts
│   │   └── node_modules/               # App-specific deps
│   │
│   ├── legacy-app/                     # React 18 application
│   │   ├── src/
│   │   │   ├── app/
│   │   │   └── main.tsx
│   │   ├── package.json                # React 18.2.0, lodash 4.17.0
│   │   ├── project.json
│   │   ├── webpack.config.js
│   │   └── node_modules/
│   │
│   ├── admin-portal/                   # React 19 with different packages
│   │   ├── src/
│   │   ├── package.json                # React 19.2.0, Material-UI 6
│   │   ├── project.json
│   │   └── node_modules/
│   │
│   ├── ui-modern/                      # UI lib for React 19
│   │   ├── src/
│   │   │   ├── index.ts
│   │   │   └── lib/
│   │   │       ├── Button/
│   │   │       └── Card/
│   │   ├── package.json                # peerDeps: React 19
│   │   ├── project.json
│   │   └── node_modules/
│   │
│   ├── ui-legacy/                      # UI lib for React 18
│   │   ├── src/
│   │   │   ├── index.ts
│   │   │   └── lib/
│   │   ├── package.json                # peerDeps: React 18
│   │   ├── project.json
│   │   └── node_modules/
│   │
│   ├── shared-utils/                   # Framework-agnostic utils
│   │   ├── src/
│   │   │   ├── index.ts
│   │   │   └── lib/
│   │   ├── package.json                # No React dependency
│   │   └── project.json
│   │
│   └── shared-types/                   # TypeScript types
│       ├── src/
│       │   └── index.ts
│       ├── package.json
│       └── project.json
│
├── .github/
│   └── workflows/
│       └── ci.yml
│
├── node_modules/                       # Shared dev dependencies only
├── dist/
├── nx.json
├── package.json                        # Root-level dev dependencies
├── pnpm-workspace.yaml                 # or yarn workspaces config
├── tsconfig.base.json
└── README.md
```

### Real-World Example: Multi-Version Dependency Configuration

**Root** `package.json` (Package-Based):

```json
{
  "name": "my-workspace",
  "private": true,
  "workspaces": [
    "packages/*"
  ],
  "devDependencies": {
    "@nx/react": "^21.6.4",
    "@nx/vite": "^21.6.4",
    "@nx/webpack": "^21.6.4",
    "typescript": "^5.3.0",
    "vitest": "^1.0.0",
    "eslint": "^8.55.0",
    "prettier": "^3.1.0"
  }
}
```

**Modern App** `package.json`:

```json
{
  "name": "@my-workspace/modern-app",
  "version": "3.0.0",
  "dependencies": {
    "react": "^19.2.0",
    "react-dom": "^19.2.0",
    "react-router-dom": "^6.20.0",
    "@tanstack/react-query": "^5.0.0",
    "@my-workspace/ui-modern": "*",
    "@my-workspace/shared-utils": "*"
  }
}
```

**Legacy App** `package.json`:

```json
{
  "name": "@my-workspace/legacy-app",
  "version": "2.14.5",
  "dependencies": {
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "react-router-dom": "^6.20.0",
    "redux": "^4.2.0",
    "lodash": "^4.17.21",
    "@my-workspace/ui-legacy": "*",
    "@my-workspace/shared-utils": "*"
  }
}
```

**Admin Portal** `package.json` (Different UI library):

```json
{
  "name": "@my-workspace/admin-portal",
  "version": "1.5.2",
  "dependencies": {
    "react": "^19.2.0",
    "react-dom": "^19.2.0",
    "@mui/material": "^6.0.0",
    "@emotion/react": "^11.11.0",
    "recharts": "^2.10.0",
    "@my-workspace/shared-utils": "*"
  }
}
```

### Key Structure Highlights

**Apps Directory**: Contains deployable applications. Each app has its own configuration and can be built independently.

**Libs/Packages Directory**: Contains reusable code. In integrated mode, organized by scope (e.g., `shared`). In package-based mode, each package is independent.

**Project Configuration**: Each project has a `project.json` file defining its build, test, lint, and serve targets.

**Path Mappings**: The `tsconfig.base.json` creates clean imports:

```javascript
// Instead of: import { Button } from '../../../libs/shared/ui-components/src/lib/button/button'
// You write:
import { Button } from '@my-workspace/ui-components';

// Package-based with version-specific libraries:
import { Button } from '@my-workspace/ui-modern';  // For React 19 apps
import { Button } from '@my-workspace/ui-legacy';  // For React 18 apps
```

**Dependency Graph**: NX tracks dependencies between projects. For example:

* **Integrated**: `customer-portal` → `ui-components`, `state`, `utils`
    
* **Package-Based**:
    
    * `modern-app` (React 19) → `ui-modern`, `shared-utils`
        
    * `legacy-app` (React 18) → `ui-legacy`, `shared-utils`
        
    * `admin-portal` (React 19) → `shared-utils` (uses MUI instead of custom UI)
        

This structure scales from small teams to large enterprises, keeping your codebase organized, maintainable, and performant while supporting multiple React versions and varying dependency requirements.

## Conclusion

NX has matured into a comprehensive monorepo solution that dramatically improves developer productivity. With intelligent caching, affected-only commands, and modern tooling integration, it's an excellent choice for teams managing multiple related projects. Start small with a single app and library, then scale as your needs grow.

For more information, visit the [official NX documentation](https://nx.dev).
