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:
Intelligent Caching: NX automatically caches task results locally and remotely, dramatically reducing build and test times
Smart Builds: Only rebuilds projects affected by your changes, saving significant CI/CD time
Dependency Visualization: Interactive dependency graphs help you understand your codebase architecture
Modern Tooling: Built-in support for Jest, Vitest, Cypress, Playwright, and Storybook
Framework Agnostic: Supports React, Angular, Vue, Node.js, and many other frameworks
Code Generation: Powerful generators create consistent, well-structured code across your workspace
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.jsonSingle 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.jsonwith specific versionsBetter 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:
npx create-nx-workspace@latest my-workspace
During setup, you'll be prompted to choose:
Preset: Select
react-monorepofor a React-focused workspaceBundler: Choose
vite(recommended) orwebpackTest Runner: Select
vitest(modern) orjestNx Cloud: Enable for remote caching (recommended for teams)
For an integrated workspace (default), your structure will look like:
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:
# 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:
// 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:
// 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:
# Create a package-based workspace
npx create-nx-workspace@latest my-workspace --preset=npm
In this setup, each application has its own package.json:
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):
{
"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):
{
"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:
// 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:
# 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:
# 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, ornone(for non-buildable libraries)--unitTestRunner:vitest,jest, ornone
Creating Version-Specific Libraries:
When managing apps with different React versions, create separate libraries for each version:
# 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:
// 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:
# 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
--exportis used)
Working with State Management
Redux Toolkit
NX provides a Redux slice generator:
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:
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:
npx nx generate @nx/react:library styles --directory=libs/shared --style=scss --bundler=none
Configure in your app's project.json:
{
"targets": {
"build": {
"options": {
"styles": ["libs/shared/styles/src/lib/global.scss"],
"stylePreprocessorOptions": {
"includePaths": ["libs/shared/styles/src/lib"]
}
}
}
}
}
Then import in your components:
@import "variables";
@import "mixins";
.my-component {
background: $primary-color;
}
Shared Assets
Configure assets in project.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:
# 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:
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:
# 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:
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:
# 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
# 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:
{
"targets": {
"build": {
"configurations": {
"production": {
"optimization": true,
"extractCss": true,
"sourceMap": false,
"namedChunks": false
}
}
}
}
}
Essential NX Commands
Workspace Operations
# 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:
# 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
# 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:
{
"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:
import { Button } from '@my-workspace/ui-components';
import { formatDate } from '@my-workspace/utils';
CI/CD Integration
NX automatically generates GitHub Actions configuration:
# .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
Enable Nx Cloud: Provides remote caching and distributed task execution
Use Affected Commands: Only build/test what changed
Configure Caching: Ensure all targets are cacheable in
nx.jsonUse Vite: Much faster than webpack for development and builds
Parallel Execution: NX automatically runs tasks in parallel when possible
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_modulessize❌ 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:
Package Names:
@nrwl/*→@nx/*Commands:
nx affected:test→nx affected --target=testBundlers: Consider migrating from webpack to Vite
Test Runners: Consider migrating from Jest to Vitest
Configuration:
workspace.jsondeprecated, useproject.jsonfiles
Run the migration command:
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)
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:
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):
{
"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:
{
"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:
{
"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):
{
"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:
// 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,utilsPackage-Based:
modern-app(React 19) →ui-modern,shared-utilslegacy-app(React 18) →ui-legacy,shared-utilsadmin-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.




