Nx Workspace: Advanced Architectures & Production Mastery (Latest Version)

Nx Workspace: Advanced Architectures & Production Mastery (Latest Version)

Welcome back, seasoned Nx developer! You’ve successfully navigated the beginner terrain, building multi-framework applications within a monorepo and experiencing the fundamental power of Nx. Now, it’s time to ascend. This document is your comprehensive, hands-on guide to mastering advanced Nx concepts, enabling you to build, manage, and deploy large-scale, enterprise-grade monorepos with confidence.

We’ll move beyond the basics, diving deep into custom tooling, sophisticated architectural patterns like Module Federation, optimizing your CI/CD pipelines with Nx Cloud, crafting robust release strategies, tuning performance, and, crucially, deploying your monorepo applications to production environments like AWS and Azure using GitHub Actions. Every concept will be reinforced with practical commands, detailed code examples, and expected outputs, ensuring a true “learn by doing” experience.

Let’s elevate your Nx expertise!


1. Introduction to Advanced Nx Concepts

You’ve experienced Nx’s core benefits: unified tooling, efficient code sharing, and intelligent task execution. Now, we unlock the next level. Advanced Nx isn’t just about speed; it’s about control, consistency, architectural enforcement, and seamless deployment at scale.

What Constitutes “Advanced” in Nx?

“Advanced” Nx refers to leveraging its extensible plugin system, sophisticated build optimizations, and specialized architectural patterns to solve complex enterprise challenges. This includes:

  • Custom Tooling: Building tailored generators and executors to codify organizational best practices and automate unique workflows.
  • Modular Architectures: Implementing dynamic micro-frontend setups using Module Federation for independent teams and deployments.
  • Hyper-Optimized CI/CD: Integrating Nx Cloud for distributed task execution, remote caching, and advanced self-healing capabilities to minimize pipeline times.
  • Robust Release Management: Defining comprehensive versioning and publishing strategies for internal and external libraries.
  • Performance Engineering: Deep-diving into the dependency graph, task inputs/outputs, and test splitting to achieve maximum build and test speeds.
  • Polyglot Integration: Extending Nx to manage non-JavaScript projects (like Java/Spring Boot) within the same monorepo.
  • Production Deployment: Understanding and implementing strategies for deploying various applications from a monorepo to cloud providers using modern CI/CD tools.

Why Delve into Advanced Nx?

For organizations with growing engineering teams, complex product suites, and a need for speed and consistency, advanced Nx becomes indispensable:

  • Standardization: Enforce consistent architecture and coding patterns across dozens or hundreds of projects.
  • Team Autonomy: Enable multiple teams to work on different parts of the monorepo, even deploying independently, without stepping on each other’s toes.
  • Cost Efficiency: Drastically reduce CI/CD infrastructure costs and developer waiting times by eliminating redundant work.
  • Maintainability: Simplify large-scale refactors and upgrades through automated migrations and well-defined boundaries.
  • Innovation: Free up developers to focus on features by automating boilerplate, configuration, and build optimizations.

Prerequisites for this Guide

  • A solid understanding of basic Nx concepts (projects, generators, executors, dependency graph, affected commands).
  • Familiarity with modern web development (TypeScript, React, Angular, Node.js).
  • Comfortable with Git and command-line interfaces.
  • A basic understanding of CI/CD principles.

2. Custom Nx Generators and Executors

The true power of Nx’s extensibility lies in its plugin system, allowing you to create your own custom generators and executors. This is how you embed your organization’s unique best practices, standards, and automation directly into your development workflow.

We’ll start by creating a local plugin, then build a custom generator to scaffold a React component library with specific default settings and an executor to perform a custom build step.

Project Setup:

Let’s create a fresh Nx workspace for this advanced journey.

  1. Create a new workspace:

    npx create-nx-workspace@latest advanced-nx-ws
    
    • Choose None for the stack, Yes for Prettier, Do it later for CI, No for Nx Cloud.
    ✔ Where would you like to create your workspace? · advanced-nx-ws
    ✔ Which stack would you like to use? · None
    ✔ Would you like to use Prettier for code formatting? (Y/n) · Yes
    ✔ Which CI provider would you like to use? · Do it later
    ✔ Would you like remote caching to make your build faster? (Y/n) · No
    
  2. Navigate into your workspace:

    cd advanced-nx-ws
    
  3. Install necessary plugins for tooling and React:

    npm install -D @nx/plugin @nx/react @nx/js
    
    • @nx/plugin: Provides generators for creating new plugins.
    • @nx/react: Needed to build React projects.
    • @nx/js: General JavaScript/TypeScript utilities.

2.1. Creating a Local Plugin

An Nx plugin is essentially a library that contains generators and/or executors. We’ll start by creating our plugin project.

  1. Generate a new Nx plugin: We’ll place our custom plugin under a tools directory, which is a common convention.

    nx g @nx/plugin:plugin tools/internal-plugin --name=internal-plugin --importPath=@my-org/internal-plugin
    
    • @nx/plugin:plugin: The generator to create an Nx plugin.
    • tools/internal-plugin: The path where our plugin library will be created.
    • --name=internal-plugin: The project name.
    • --importPath=@my-org/internal-plugin: The TypeScript path alias. This allows other projects to import our custom generators/executors cleanly.

    Expected Output (creates tools/internal-plugin folder, updates nx.json, tsconfig.base.json):

    NX Generating @nx/plugin:plugin
    
    CREATE tools/internal-plugin/package.json
    CREATE tools/internal-plugin/project.json
    ... (many plugin-related files including generator stubs)
    UPDATE nx.json
    UPDATE tsconfig.base.json
    NX Successfully ran generator @nx/plugin:plugin for internal-plugin
    
    • You’ll find a generators and executors folder inside tools/internal-plugin/src/ with example stubs.

2.2. Custom Generator: Scaffold Standardized React Library

Let’s create a custom generator that scaffolds a React component library with our organization’s preferred settings (e.g., always using SCSS, always setting specific tags, and including a default component). This generator will wrap the existing @nx/react:lib generator, adding our custom logic.

  1. Generate a new generator within our plugin:

    nx g @nx/plugin:generator component-lib --project=internal-plugin
    
    • component-lib: The name of our new generator.
    • --project=internal-plugin: Specifies that this generator belongs to our internal-plugin.

    Expected Output (creates tools/internal-plugin/src/generators/component-lib folder):

    NX Generating @nx/plugin:generator
    
    CREATE tools/internal-plugin/src/generators/component-lib/generator.ts
    CREATE tools/internal-plugin/src/generators/component-lib/schema.d.ts
    CREATE tools/internal-plugin/src/generators/component-lib/schema.json
    CREATE tools/internal-plugin/src/generators/component-lib/README.md
    UPDATE tools/internal-plugin/generators.json
    NX Successfully ran generator @nx/plugin:generator for component-lib
    
  2. Define the schema for our generator (tools/internal-plugin/src/generators/component-lib/schema.json): This defines the options our generator will accept from the CLI.

    // tools/internal-plugin/src/generators/component-lib/schema.json
    {
      "$schema": "http://json-schema.org/draft-07/schema",
      "cli": "nx",
      "id": "component-lib",
      "title": "Component Library Generator",
      "type": "object",
      "properties": {
        "name": {
          "type": "string",
          "description": "The name of the component library",
          "$default": {
            "$source": "argv",
            "index": 0
          },
          "x-prompt": "What name would you like to give the component library?"
        },
        "directory": {
          "type": "string",
          "description": "A directory where the library is placed",
          "x-prompt": "In what directory should the library be placed (e.g., 'feature/my-feature')?",
          "default": "shared/components"
        },
        "addStorybook": {
          "type": "boolean",
          "description": "Add Storybook configuration to the library.",
          "default": false,
          "x-prompt": "Would you like to add Storybook configuration?"
        },
        "addJest": {
          "type": "boolean",
          "description": "Add Jest unit testing configuration.",
          "default": true,
          "x-prompt": "Would you like to add Jest unit testing?"
        }
      },
      "required": ["name"]
    }
    
    • $default and x-prompt make the generator interactive in the CLI or Nx Console.
  3. Implement the generator logic (tools/internal-plugin/src/generators/component-lib/generator.ts): This file orchestrates calling other Nx generators and modifying files.

    // tools/internal-plugin/src/generators/component-lib/generator.ts
    import { Tree, formatFiles, installPackagesTask, runTasksInSerial } from '@nx/devkit';
    import { libraryGenerator as reactLibraryGenerator } from '@nx/react';
    import { componentGenerator as reactComponentGenerator } from '@nx/react';
    import { storybookConfigurationGenerator } from '@nx/react'; // For Storybook
    import { jestProjectGenerator } from '@nx/jest'; // For Jest
    
    import { ComponentLibGeneratorSchema } from './schema';
    
    export async function componentLibGenerator(tree: Tree, options: ComponentLibGeneratorSchema) {
      const { name, directory, addStorybook, addJest } = options;
      const tasks = [];
    
      // 1. Generate the base React library with opinionated settings
      const libOptions = {
        name,
        directory,
        tags: `scope:${directory.split('/')[0]},type:ui`, // Auto-generate scope and type tags
        style: 'scss', // Always use SCSS
        unitTestRunner: 'none' as const, // We'll add Jest explicitly if requested
        bundler: 'rollup' as const, // Or 'vite', 'webpack' depending on preference
        publishable: true, // Always publishable for shared components
        importPath: `@my-org/${directory}/${name}`, // Standardized import path
        minimal: true, // Start with a minimal library setup
      };
      tasks.push(await reactLibraryGenerator(tree, libOptions));
    
      // 2. Add Jest if requested
      if (addJest) {
        tasks.push(await jestProjectGenerator(tree, { project: `${directory}-${name}` }));
      }
    
      // 3. Add a default component to the new library
      tasks.push(
        await reactComponentGenerator(tree, {
          name: `${name}-component`, // e.g., my-button-component
          project: `${directory}-${name}`, // The generated lib project name
          directory: 'components', // Place it under a 'components' folder
          export: true, // Export it from the library's index.ts
          style: 'scss',
        })
      );
    
      // 4. Configure Storybook if requested
      if (addStorybook) {
        // Need to install @nx/storybook first if it's not present globally or in root package.json
        tasks.push(
          await storybookConfigurationGenerator(tree, {
            name: `${directory}-${name}`,
            uiFramework: '@storybook/react-vite', // Or '@storybook/react-webpack5'
            bundler: 'vite', // Match the library's bundler
            configureStaticServe: true,
            skipFormat: true,
          })
        );
      }
    
      await formatFiles(tree);
    
      // Install packages for newly added dev dependencies (e.g., Storybook, Jest)
      tasks.push(installPackagesTask(tree));
    
      return runTasksInSerial(...tasks);
    }
    
    • Key Devkit functions:
      • Tree: An in-memory representation of your workspace. Generators operate on this Tree, allowing Nx to preview changes (--dry-run) before writing them to disk.
      • formatFiles: Runs Prettier across affected files.
      • installPackagesTask: Installs any new npm dependencies added by the generator.
      • runTasksInSerial: Allows combining multiple returned tasks (like package installation) into one sequential execution.
  4. Test our custom generator:

    • Dry run first (always a good idea!):

      nx g @my-org/internal-plugin:component-lib my-button --directory=design-system/atoms --addStorybook --dry-run
      

      Expected Output (lots of CREATE, UPDATE logs, but nothing is written to disk):

      NX Generating @my-org/internal-plugin:component-lib
      
      CREATE libs/design-system/atoms/my-button/project.json
      CREATE libs/design-system/atoms/my-button/src/index.ts
      ... (many other files for lib, component, storybook, jest)
      UPDATE nx.json
      UPDATE tsconfig.base.json
      UPDATE package.json
      NX Successfully ran generator @my-org/internal-plugin:component-lib --dry-run
      
    • Run for real:

      nx g @my-org/internal-plugin:component-lib my-button --directory=design-system/atoms --addStorybook --addJest
      
      • You’ll see the files created under libs/design-system/atoms/my-button.
      • Check nx.json and tsconfig.base.json for updates.
      • Run nx graph and you’ll see your new design-system-atoms-my-button library.

2.3. Custom Executor: Version Bumping and Syncing

Executors perform actions. Let’s create a custom executor that, for a given library, reads its version from package.json, bumps it, and then updates a specific version file (e.g., src/version.ts) within the library. This is a common pattern for internal libraries that need version tracking.

  1. Generate a new executor within our plugin:

    nx g @nx/plugin:executor bump-version --project=internal-plugin
    
    • bump-version: The name of our new executor.

    Expected Output (creates tools/internal-plugin/src/executors/bump-version folder):

    NX Generating @nx/plugin:executor
    
    CREATE tools/internal-plugin/src/executors/bump-version/executor.ts
    CREATE tools/internal-plugin/src/executors/bump-version/schema.d.ts
    CREATE tools/internal-plugin/src/executors/bump-version/schema.json
    UPDATE tools/internal-plugin/executors.json
    NX Successfully ran generator @nx/plugin:executor for bump-version
    
  2. Define the schema for our executor (tools/internal-plugin/src/executors/bump-version/schema.json):

    // tools/internal-plugin/src/executors/bump-version/schema.json
    {
      "$schema": "http://json-schema.org/draft-07/schema",
      "id": "BumpVersion",
      "title": "Bump Version Executor",
      "type": "object",
      "properties": {
        "project": {
          "type": "string",
          "description": "The name of the project to bump the version for.",
          "$default": {
            "$source": "projectName"
          }
        },
        "dryRun": {
          "type": "boolean",
          "description": "Perform a dry run without modifying files.",
          "default": false
        },
        "increment": {
          "type": "string",
          "description": "The version increment (patch, minor, major).",
          "enum": ["patch", "minor", "major"],
          "default": "patch"
        }
      },
      "required": ["project"]
    }
    
  3. Implement the executor logic (tools/internal-plugin/src/executors/bump-version/executor.ts):

    // tools/internal-plugin/src/executors/bump-version/executor.ts
    import { ExecutorContext, logger, readJsonFile, writeJsonFile, readProjectConfiguration } from '@nx/devkit';
    import { resolve } from 'path';
    import { readFileSync, writeFileSync } from 'fs';
    import semver from 'semver'; // You'll need to install 'semver' if not already present: npm i semver
    import { BumpVersionExecutorSchema } from './schema';
    
    export default async function runExecutor(
      options: BumpVersionExecutorSchema,
      context: ExecutorContext
    ) {
      const { project, dryRun, increment } = options;
      const projectName = context.projectName || project;
    
      if (!projectName) {
        logger.error('Project name not found in context or options.');
        return { success: false };
      }
    
      const projectConfig = readProjectConfiguration(context.tree, projectName);
      const projectRoot = projectConfig.root;
      const packageJsonPath = resolve(context.root, projectRoot, 'package.json');
      const versionFilePath = resolve(context.root, projectRoot, 'src/version.ts');
    
      logger.info(`Bumping version for project: ${projectName}`);
      logger.info(`Project root: ${projectRoot}`);
      logger.info(`Package.json path: ${packageJsonPath}`);
      logger.info(`Version file path: ${versionFilePath}`);
    
      try {
        // 1. Read package.json
        const packageJson = readJsonFile(packageJsonPath);
        let currentVersion = packageJson.version;
    
        if (!currentVersion) {
          logger.error(`No version found in ${packageJsonPath}.`);
          return { success: false };
        }
    
        const newVersion = semver.inc(currentVersion, increment);
        if (!newVersion) {
          logger.error(`Failed to increment version ${currentVersion} with ${increment}.`);
          return { success: false };
        }
    
        logger.info(`Current version: ${currentVersion}`);
        logger.info(`New version: ${newVersion}`);
    
        // 2. Update package.json
        if (!dryRun) {
          packageJson.version = newVersion;
          writeJsonFile(packageJsonPath, packageJson);
          logger.info(`Updated version in ${packageJsonPath}`);
        } else {
          logger.info(`(Dry Run) Would update version in ${packageJsonPath}`);
        }
    
        // 3. Update version.ts file
        const versionFileContent = `export const version = '${newVersion}';\n`;
        if (!dryRun) {
          writeFileSync(versionFilePath, versionFileContent, 'utf-8');
          logger.info(`Updated version in ${versionFilePath}`);
        } else {
          logger.info(`(Dry Run) Would update version in ${versionFilePath}`);
        }
    
        return { success: true };
      } catch (error) {
        logger.error(`Error bumping version: ${error.message}`);
        return { success: false };
      }
    }
    
    • Dependency: semver (a popular npm package for semantic versioning). Install it: npm install semver
    • Key Devkit functions:
      • ExecutorContext: Provides context about the current execution (project name, workspace root, Tree).
      • logger: For consistent logging in Nx.
      • readJsonFile, writeJsonFile: Utility functions for working with JSON files.
      • readProjectConfiguration: To get details about the target project.
  4. Add the custom executor to design-system-atoms-my-button’s project.json: We need to tell Nx that our my-button library has a new bump-version target using our custom executor.

    • Open libs/design-system/atoms/my-button/project.json.
    • Add a new target under targets:
      // libs/design-system/atoms/my-button/project.json (partial)
      {
        "targets": {
          "build": { /* ... */ },
          "test": { /* ... */ },
          "lint": { /* ... */ },
          "bump-version": {
            "executor": "@my-org/internal-plugin:bump-version",
            "options": {
              "project": "design-system-atoms-my-button",
              "increment": "patch"
            },
            "outputs": [] // This target doesn't produce files in the dist folder, but updates source files
          }
        }
      }
      
    • Create the initial src/version.ts file in your my-button library:
      // libs/design-system/atoms/my-button/src/version.ts
      export const version = '0.0.0';
      
    • Update libs/design-system/atoms/my-button/package.json to have an initial version:
      // libs/design-system/atoms/my-button/package.json (partial)
      {
        "name": "@my-org/design-system-atoms-my-button",
        "version": "0.0.0", // <--- Add this line
        // ...
      }
      
  5. Test our custom executor:

    • Dry run:

      nx bump-version design-system-atoms-my-button --dry-run --increment=minor
      

      Expected Output:

      NX Running target bump-version for project design-system-atoms-my-button
      NX   Bumping version for project: design-system-atoms-my-button
      NX   Project root: libs/design-system/atoms/my-button
      NX   Package.json path: /path/to/advanced-nx-ws/libs/design-system/atoms/my-button/package.json
      NX   Version file path: /path/to/advanced-nx-ws/libs/design-system/atoms/my-button/src/version.ts
      NX   Current version: 0.0.0
      NX   New version: 0.1.0
      NX   (Dry Run) Would update version in /path/to/advanced-nx-ws/libs/design-system/atoms/my-button/package.json
      NX   (Dry Run) Would update version in /path/to/advanced-nx-ws/libs/design-system/atoms/my-button/src/version.ts
      NX Ran target bump-version for project design-system-atoms-my-button
      
    • Run for real:

      nx bump-version design-system-atoms-my-button --increment=minor
      

      Expected Output (check files for changes):

      NX Running target bump-version for project design-system-atoms-my-button
      NX   Bumping version for project: design-system-atoms-my-button
      NX   Project root: libs/design-system/atoms/my-button
      NX   Package.json path: /path/to/advanced-nx-ws/libs/design-system/atoms/my-button/package.json
      NX   Version file path: /path/to/advanced-nx-ws/libs/design-system/atoms/my-button/src/version.ts
      NX   Current version: 0.0.0
      NX   New version: 0.1.0
      NX   Updated version in /path/to/advanced-nx-ws/libs/design-system/atoms/my-button/package.json
      NX   Updated version in /path/to/advanced-nx-ws/libs/design-system/atoms/my-button/src/version.ts
      NX Ran target bump-version for project design-system-atoms-my-button
      
      • Verify that libs/design-system/atoms/my-button/package.json now has "version": "0.1.0" and libs/design-system/atoms/my-button/src/version.ts contains export const version = '0.1.0';.

3. Module Federation with Nx: Building Scalable Micro-Frontends

Module Federation allows you to build independently deployable “micro-frontends” (or “remotes”) that can be composed at runtime into a larger “host” application. Nx provides powerful first-class support for Module Federation, making it significantly easier to manage this complex architecture within a monorepo.

We’ll build a simplified e-commerce dashboard with:

  • A Host App (admin-shell)
  • A Products Micro-Frontend (product-mfe)
  • An Orders Micro-Frontend (order-mfe)
  • Both micro-frontends will be React applications, sharing our custom design-system-atoms-my-button library.

Project Setup (continuing from previous section in advanced-nx-ws):

  1. Install React and Rspack plugins for Module Federation: You should already have @nx/react installed. Rspack is a next-generation bundler that is becoming a strong alternative to Webpack, and Nx has excellent Module Federation support for it.

    npm install -D @nx/rspack
    
  2. Ensure @nx/react is updated for Rspack support (if needed):

    nx migrate latest --exclude-libs-with-migrations=true --commit=false --run-migrations=false
    # Review changes, then run npm install
    npm install
    
    • This is a general step to ensure your Nx plugins are up-to-date with the latest features.

3.1. Generating a Module Federation Host Application

The host application is the main entry point that loads and orchestrates your micro-frontends.

  1. Generate a React host application:

    nx g @nx/react:host admin-shell --remotes=product-mfe,order-mfe --bundler=rspack --style=css --e2e-test-runner=none
    
    • @nx/react:host: The generator for a React host application.
    • admin-shell: Our host application’s name.
    • --remotes=product-mfe,order-mfe: Crucially, this tells Nx to automatically configure admin-shell to consume two remote applications named product-mfe and order-mfe. It will also generate these remote applications.
    • --bundler=rspack: Use Rspack as the bundler. Nx also supports Webpack.
    • --e2e-test-runner=none: Skip E2E tests for brevity.

    Expected Output (creates apps/admin-shell, apps/product-mfe, apps/order-mfe and their configurations):

    NX Generating @nx/react:host
    
    ✔ Which stylesheet format would you like to use? · css
    ✔ Which E2E test runner would you like to use? · none
    Fetching @nx/rspack...
    Fetching @nx/jest...
    ... (many files created for shell and two remotes)
    UPDATE nx.json
    UPDATE package.json
    UPDATE tsconfig.base.json
    NX Successfully ran generator @nx/react:host for admin-shell
    
    • Nx has created three applications! admin-shell, product-mfe, and order-mfe.
    • It also configured Module Federation in their respective module-federation.config.ts files.

3.2. Implementing Micro-Frontends and Sharing a Library

Now let’s populate our micro-frontends and demonstrate sharing our custom my-button library from design-system-atoms-my-button.

  1. Use design-system-atoms-my-button in product-mfe:

    • Open apps/product-mfe/src/app/app.tsx.
    • Modify it to use our shared Button component and mock some product data.
      // apps/product-mfe/src/app/app.tsx
      import { Button } from '@my-org/design-system/atoms/my-button'; // Import our shared button!
      import styles from './app.module.css';
      
      interface Product {
        id: string;
        name: string;
        price: number;
      }
      
      const products: Product[] = [
        { id: '1', name: 'MF Smartwatch', price: 199.99 },
        { id: '2', name: 'MF Wireless Headphones', price: 99.99 },
      ];
      
      export function App() {
        const handleViewDetails = (product: Product) => {
          alert(`Viewing details for ${product.name} (from Product MFE)`);
        };
      
        return (
          <div className={styles['container']}>
            <h2>Product Catalog (Micro-Frontend)</h2>
            <p>This content is dynamically loaded!</p>
            <div className={styles['productGrid']}>
              {products.map((product) => (
                <div key={product.id} className={styles['productCard']}>
                  <h3>{product.name}</h3>
                  <p>${product.price.toFixed(2)}</p>
                  <Button label="View Details" onClick={() => handleViewDetails(product)} />
                </div>
              ))}
            </div>
          </div>
        );
      }
      
      export default App;
      
    • Edit apps/product-mfe/src/app/app.module.css:
      /* apps/product-mfe/src/app/app.module.css */
      .container {
        padding: 20px;
        border: 2px dashed #007bff;
        border-radius: 8px;
        margin: 20px;
        background-color: #e6f2ff;
      }
      .container h2 {
        color: #0056b3;
        margin-top: 0;
      }
      .productGrid {
        display: grid;
        grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
        gap: 15px;
        margin-top: 20px;
      }
      .productCard {
        background-color: white;
        border: 1px solid #cce0ff;
        border-radius: 6px;
        padding: 15px;
        box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
        text-align: center;
      }
      .productCard h3 {
        font-size: 1.2rem;
        margin-top: 0;
        color: #333;
      }
      .productCard p {
        color: #666;
        margin-bottom: 15px;
      }
      
  2. Use design-system-atoms-my-button in order-mfe:

    • Open apps/order-mfe/src/app/app.tsx.
    • Modify it similarly to use the Button component and mock some order data.
      // apps/order-mfe/src/app/app.tsx
      import { Button } from '@my-org/design-system/atoms/my-button'; // Import our shared button!
      import styles from './app.module.css';
      
      interface Order {
        id: string;
        customer: string;
        status: string;
        total: number;
      }
      
      const orders: Order[] = [
        { id: 'ORD001', customer: 'Alice', status: 'Pending', total: 329.98 },
        { id: 'ORD002', customer: 'Bob', status: 'Shipped', total: 99.99 },
      ];
      
      export function App() {
        const handleViewOrder = (order: Order) => {
          alert(`Viewing order ${order.id} for ${order.customer} (from Order MFE)`);
        };
      
        return (
          <div className={styles['container']}>
            <h2>Recent Orders (Micro-Frontend)</h2>
            <p>This is another piece of our dynamic dashboard!</p>
            <div className={styles['orderList']}>
              {orders.map((order) => (
                <div key={order.id} className={styles['orderCard']}>
                  <h3>Order #{order.id}</h3>
                  <p>Customer: {order.customer}</p>
                  <p>Status: <span className={styles[order.status.toLowerCase()]}>{order.status}</span></p>
                  <p>Total: ${order.total.toFixed(2)}</p>
                  <Button label="View Order" onClick={() => handleViewOrder(order)} variant="secondary" />
                </div>
              ))}
            </div>
          </div>
        );
      }
      
      export default App;
      
    • Edit apps/order-mfe/src/app/app.module.css:
      /* apps/order-mfe/src/app/app.module.css */
      .container {
        padding: 20px;
        border: 2px dashed #28a745;
        border-radius: 8px;
        margin: 20px;
        background-color: #e0ffe6;
      }
      .container h2 {
        color: #1a7e3d;
        margin-top: 0;
      }
      .orderList {
        display: grid;
        grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
        gap: 15px;
        margin-top: 20px;
      }
      .orderCard {
        background-color: white;
        border: 1px solid #c7e6d0;
        border-radius: 6px;
        padding: 15px;
        box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
        text-align: center;
      }
      .orderCard h3 {
        font-size: 1.2rem;
        margin-top: 0;
        color: #333;
      }
      .orderCard p {
        color: #666;
        margin-bottom: 10px;
      }
      .order-status {
        font-weight: bold;
      }
      .pending {
        color: #ffc107;
      }
      .shipped {
        color: #17a2b8;
      }
      .delivered {
        color: #28a745;
      }
      
  3. Configure the Host (admin-shell) to display remotes:

    • Open apps/admin-shell/src/app/app.tsx.
    • The generator already set up basic routing for remotes. Modify it to include our shared UI and a simple layout.
      // apps/admin-shell/src/app/app.tsx
      import { useEffect, Suspense } from 'react';
      import { Link, Route, Routes } from 'react-router-dom';
      import { Button } from '@my-org/design-system/atoms/my-button'; // Shared UI
      import styles from './app.module.css';
      
      // Lazy load the remote components
      const ProductMfe = ({ name }: { name: string }) => {
        useEffect(() => {
          console.log(`ProductMfe loaded with name: ${name}`);
        }, [name]);
        return (
          <Suspense fallback={<div>Loading {name} MFE...</div>}>
            <InnerProductMfe />
          </Suspense>
        );
      };
      const InnerProductMfe = React.lazy(() => import('product-mfe/Module'));
      
      const OrderMfe = ({ name }: { name: string }) => {
        useEffect(() => {
          console.log(`OrderMfe loaded with name: ${name}`);
        }, [name]);
        return (
          <Suspense fallback={<div>Loading {name} MFE...</div>}>
            <InnerOrderMfe />
          </Suspense>
        );
      };
      const InnerOrderMfe = React.lazy(() => import('order-mfe/Module'));
      
      
      export function App() {
        return (
          <div className={styles['host-container']}>
            <header className={styles['header']}>
              <h1>Admin Dashboard Host</h1>
              <nav className={styles['navigation']}>
                <Link to="/products">
                  <Button text="Products MFE" onClick={() => {}} />
                </Link>
                <Link to="/orders">
                  <Button text="Orders MFE" onClick={() => {}} variant="secondary" />
                </Link>
              </nav>
            </header>
            <main className={styles['main-content']}>
              <Suspense fallback={<div>Loading router...</div>}>
                <Routes>
                  <Route path="/" element={<h2>Welcome to the Admin Dashboard! Select a section above.</h2>} />
                  <Route path="/products" element={<ProductMfe name="product-mfe" />} />
                  <Route path="/orders" element={<OrderMfe name="order-mfe" />} />
                </Routes>
              </Suspense>
            </main>
          </div>
        );
      }
      
      export default App;
      
    • Edit apps/admin-shell/src/app/app.module.css:
      /* apps/admin-shell/src/app/app.module.css */
      .host-container {
        font-family: Arial, sans-serif;
        background-color: #f8f9fa;
        min-height: 100vh;
      }
      
      .header {
        background-color: #343a40;
        color: white;
        padding: 20px;
        display: flex;
        justify-content: space-between;
        align-items: center;
        box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
      }
      
      .header h1 {
        margin: 0;
        font-size: 2rem;
      }
      
      .navigation {
        display: flex;
        gap: 10px;
      }
      
      .main-content {
        padding: 20px;
        background-color: #ffffff;
        margin: 20px;
        border-radius: 8px;
        box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
        min-height: calc(100vh - 180px); /* Adjust based on header/footer height */
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
      }
      
      .main-content h2 {
        color: #495057;
        font-size: 1.8rem;
        margin-bottom: 30px;
        text-align: center;
      }
      
      /* Override default button margin from shared-ui for navigation */
      .navigation .button {
        margin: 0;
      }
      

3.3. Running and Observing Module Federation

Nx makes running Module Federation locally quite elegant.

  1. Serve the host application:
    nx serve admin-shell
    
    Expected Output (Nx will automatically serve product-mfe and order-mfe statically!):
    NX Starting type checking for admin-shell
    NX Running @nx/react:dev-server for admin-shell
    
    > admin-shell@0.0.1 serve /path/to/advanced-nx-ws
    > rspack serve --config ./apps/admin-shell/rspack.config.js
    
    NX Compiling for admin-shell...
    ... (Rspack compilation logs for admin-shell, product-mfe, order-mfe) ...
    NX The build artifacts can be found in the dist/apps/admin-shell folder.
    NX Web Development Server for admin-shell is listening on http://localhost:4200
    NX Compiled successfully.
    
    • Notice that you only ran nx serve admin-shell, but Nx detected the remote dependencies and served product-mfe and order-mfe as well, usually statically or by proxying to their own dev servers if specified with devRemotes.
    • Open http://localhost:4200 in your browser.
    • Click the “Products MFE” and “Orders MFE” buttons. You’ll see the content of each micro-frontend loaded dynamically into the host application!
    • The buttons within the MFE are from your shared design-system-atoms-my-button library.

Key Learnings for Module Federation:

  • Runtime Composition: Micro-frontends are loaded at runtime, not build time, allowing independent deployments.
  • Shared Dependencies: Nx intelligently handles shared dependencies (like React itself, or our design-system-atoms-my-button library), ensuring they are loaded only once.
  • Team Autonomy: Different teams can own and deploy their micro-frontends independently. The host simply needs to know where to find them.
  • Dynamic vs. Static Federation: Nx supports both. In our example, admin-shell statically knows about product-mfe and order-mfe. For dynamic federation, you’d load remote entry points based on a registry at runtime (a more advanced topic for discovery services).

4. Advanced CI/CD Pipelines with Nx Cloud

Optimizing CI/CD is critical for large monorepos. Nx Cloud provides powerful features like remote caching and distributed task execution (DTE) to dramatically cut down CI times and enhance developer feedback.

We’ll set up a GitHub Actions workflow that leverages Nx Cloud for a significantly faster CI pipeline.

Prerequisites:

  • A GitHub account.
  • An Nx Cloud account (free for open-source and small teams).
    • If you haven’t connected:
      npx nx connect
      
      Follow the prompts to connect your advanced-nx-ws to Nx Cloud.

4.1. The Standard CI Workflow (Pre-Nx Cloud)

Let’s first generate a basic GitHub Actions workflow file to see the starting point.

  1. Generate a CI workflow for GitHub Actions:

    nx g @nx/workspace:ci-workflow --ci=github
    
    • This will create .github/workflows/ci.yml.

    Expected .github/workflows/ci.yml (initial content):

    # .github/workflows/ci.yml
    name: CI
    
    on:
      push:
        branches:
          - main
      pull_request:
    
    permissions:
      actions: read
      contents: read
    
    jobs:
      main:
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v4
            with:
              filter: tree:0
              fetch-depth: 0
    
          # Cache node_modules
          - uses: actions/setup-node@v4
            with:
              node-version: 20
              cache: 'npm'
    
          - run: npm ci
          - uses: nrwl/nx-set-shas@v4
    
          # Run lint, test, build for affected projects
          - run: npx nx affected -t lint test build
          # Nx Cloud recommends fixes for failures to help you get CI green faster. Learn more: https://nx.dev/ci/features/self-healing-ci
          # - run: npx nx fix-ci
          #   if: always()
    
    • This basic workflow uses nx affected -t lint test build, which is good, but it runs all these tasks sequentially on a single ubuntu-latest runner.

4.2. Enhancing CI with Nx Cloud (Remote Caching & DTE)

Now, let’s modify ci.yml to leverage Nx Cloud for speed.

  1. Modify .github/workflows/ci.yml: We’ll uncomment and add steps for Nx Cloud’s DTE. Replace the npx nx affected -t lint test build line.

    # .github/workflows/ci.yml
    name: CI
    
    on:
      push:
        branches:
          - main
      pull_request:
    
    # Needed for nx-set-shas when run on the main branch
    permissions:
      actions: read
      contents: read
    
    env:
      NX_CLOUD_DISTRIBUTED_EXECUTION: 'true' # <--- Enable DTE
      # NX_CLOUD_STOP_AGENTS_AFTER: 'build' # Optional: Stop agents after a specific target is done
      NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }} # <--- Your Nx Cloud token
    
    jobs:
      main:
        name: NxCloud-MainJob
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v4
            name: Checkout [Pull Request]
            if: ${{ github.event_name == 'pull_request' }}
            with:
              # By default, PRs will be checked-out based on the Merge Commit, but we want the actual branch HEAD.
              ref: ${{ github.event.pull_request.head.sha }}
              # We need to fetch all branches and commits so that Nx affected has a base to compare against.
              fetch-depth: 0
              filter: tree:0
    
          - uses: actions/checkout@v4
            name: Checkout [Default Branch]
            if: ${{ github.event_name != 'pull_request' }}
            with:
              # We need to fetch all branches and commits so that Nx affected has a base to compare against.
              fetch-depth: 0
              filter: tree:0
    
          # Use the package manager cache if available
          - uses: actions/setup-node@v4
            with:
              node-version: 20
              cache: 'npm' # or 'yarn' / 'pnpm'
    
          - name: Install dependencies
            run: npm ci
    
          - name: Checkout the default branch
            # This is critical for `nx affected` to have a correct base for comparison
            run: git branch --track main origin/main
    
          - name: Initialize the Nx Cloud distributed CI run
            run: npx nx-cloud start-ci-run --distribute-on="3 linux-medium-js" --stop-agents-after="build" # <--- Start DTE with 3 agents, stop after build
    
          - name: Lint, test, build, and run e2e
            # This command now orchestrates tasks across agents
            run: npx nx affected -t lint,test,build,e2e --configuration=ci # Assuming 'e2e' target exists for apps
    
          - name: Nx Cloud Self-Healing CI (Optional, but recommended for advanced usage)
            # This step will detect failures and propose fixes in your PR.
            run: npx nx fix-ci
            if: always() # Always run this step even if previous failed
    
      # Define agents that will execute tasks in parallel
      agents:
        name: Agent ${{ matrix.agent }}
        runs-on: ubuntu-latest
        strategy:
          matrix:
            # Add more agents here as your repository expands
            agent: [1, 2, 3] # Number of parallel agents
        steps:
          - name: Checkout
            uses: actions/checkout@v4
            with:
              fetch-depth: 0 # Needed for agent to access full history
    
          - uses: actions/setup-node@v4
            with:
              node-version: 20
              cache: 'npm'
    
          - name: Install dependencies
            run: npm ci
    
          - name: Start Nx Agent ${{ matrix.agent }}
            run: npx nx-cloud start-agent
            env:
              NX_AGENT_NAME: ${{ matrix.agent }}
    
  2. Add NX_CLOUD_ACCESS_TOKEN to GitHub Secrets:

    • Go to your GitHub repository -> Settings -> Secrets and variables -> Actions.
    • Click “New repository secret”.
    • Name: NX_CLOUD_ACCESS_TOKEN
    • Value: Paste the access token from your Nx Cloud workspace settings.
  3. Push your changes to GitHub:

    git add .github/workflows/ci.yml
    git commit -m "feat: Configure advanced CI with Nx Cloud DTE"
    git push origin main
    
    • Observe the GitHub Actions run. You should see multiple “Agent” jobs spinning up and tasks (lint, test, build) being distributed among them.
    • The overall CI time should be significantly reduced, especially if you had many projects or long-running tests.

Key Learnings for Advanced CI/CD:

  • Remote Caching: Nx Cloud automatically handles remote caching. When a task completes on any runner, its output is cached. The next time the same task on the same code runs on any other runner (even a different agent), it’s an instant cache hit.
  • Distributed Task Execution (DTE): The npx nx-cloud start-ci-run --distribute-on="..." command orchestrates splitting tasks across multiple agents. Each agent runs npx nx-cloud start-agent to register itself. Nx intelligently distributes the lint,test,build,e2e tasks among them.
  • nx-set-shas (or manual git setup): Essential for nx affected commands to correctly determine the base for comparison in CI.
  • Self-Healing CI (Optional but powerful): The nx fix-ci command, when enabled, can automatically fix common CI failures (like linting errors) and propose them as a commit, reducing developer feedback loops.

5. Nx Release Strategies

Managing versions and publishing packages in a monorepo can be complex. Nx Release provides a unified, flexible solution for this, supporting both unified (all projects same version) and independent (each project its own version) strategies.

We’ll refine our nx.json to configure an advanced release strategy, including independent versioning for our libraries and automatic changelog generation.

Prerequisites:

  • Ensure our design-system-atoms-my-button and ecom-domain libraries are marked as publishable in their project.json files. (We did this during generation in Section 3).

5.1. Configure Independent Versioning & Changelogs

For many monorepos, especially those with independently used libraries, independent versioning is preferred.

  1. Configure nx.json for independent release: Open nx.json and modify/add the release section.

    // nx.json (update or add this section)
    {
      "$schema": "./node_modules/nx/schemas/nx-schema.json",
      "npmScope": "my-org",
      // ... other configs ...
      "release": {
        "projects": ["{workspaceRoot}/libs/**"], // Target all libraries for release
        "releaseTagPattern": "{projectName}@${version}", // e.g., @my-org/shared-ui@1.0.0
        "changelog": {
          "preset": "angular", // Use Conventional Commits standard
          "column": false // Don't add a changelog column to the main Nx output
        },
        "git": {
          "commit": true,
          "commitMessage": "chore({projectName}): release ${projectName} ${version}",
          "tag": true,
          "addRemote": false // Set to true if you need to add a remote (e.g., if publishing to multiple registries)
        },
        "version": {
          "conventionalCommits": true, // Derive versions from commit messages
          "pre-release": false, // Don't automatically create pre-release versions
          "generator": "@nx/js:release-version" // Or a custom one if needed
        },
        "changelog": {
          "createRelease": "github" // Create GitHub releases with changelog notes
        },
        "git": {
          "commit": true,
          "commitMessage": "chore(${projectName}): release ${projectName} ${version}",
          "tag": true,
          "addRemote": false,
          "push": false
        },
        "pr": {
          "conventionalCommits": true,
          "name": "Release: {version}",
          "body": "Releasing {version} of {projectName}",
          "labels": ["release", "automated"],
          "base": "main",
          "targetBranch": "main"
        },
        "pipeline": {
          "publish": {
            "dependsOn": ["^build"]
          }
        }
      }
    }
    
    • "projects": ["{workspaceRoot}/libs/**"]: This tells Nx to consider all projects in the libs folder for release.
    • "releaseTagPattern": "{projectName}@${version}": Configures how Git tags are created (e.g., @my-org/design-system-atoms-my-button@1.0.0).
    • "version": { "conventionalCommits": true }: Nx will analyze your Git commit messages (following Conventional Commits spec: feat:, fix:, chore:, perf:, refactor:) to automatically determine the next semantic version bump (patch, minor, major).
    • "changelog": { "createRelease": "github" }: This is powerful! Nx will automatically create a GitHub Release with a generated changelog based on the commit messages since the last release.
    • "git": { "commit": true, "tag": true }: Nx will create a release commit and tag it.
    • "pr": Configuration for automatically creating pull requests for release, very useful for controlled release flows.

5.2. Performing a Dry Run Release

Before running a real release, always use dry-run.

  1. Make a new commit with a feat: prefix (to trigger a minor version bump):

    • Edit libs/shared/ecom-domain/src/lib/ecom-domain.ts.
    • Add a new imageUrl property to Product (if you haven’t already from previous steps).
      // libs/shared/ecom-domain/src/lib/ecom-domain.ts (add imageUrl)
      export interface Product {
        id: string;
        name: string;
        price: number;
        currency: string;
        description?: string;
        imageUrl?: string; // <-- New field
      }
      // ... rest of file
      
    • Commit this change using a conventional commit message that implies a feature.
      git add libs/shared/ecom-domain/src/lib/ecom-domain.ts
      git commit -m "feat(ecom-domain): Add imageUrl to Product interface"
      
  2. Run the Nx Release dry run:

    nx release version --dry-run
    

    Expected Output (shows version bumps for affected projects):

    NX Performing release versioning...
    
    NX   These projects will be versioned:
    
    - ecom-domain: 0.0.1 -> 0.1.0 (minor)  # <--- Should see minor bump for ecom-domain
    - admin-shell: 0.0.1 -> 0.0.1 (no increment)
    - product-mfe: 0.0.1 -> 0.0.1 (no increment)
    - order-mfe: 0.0.1 -> 0.0.1 (no increment)
    - design-system-atoms-my-button: 0.1.0 -> 0.1.0 (no increment)
    - ... other projects ...
    
    NX Performing release changelog generation...
    NX   For project ecom-domain, the changelog will include:
        - feat(ecom-domain): Add imageUrl to Product interface
    NX Performing release git operations...
    NX Performing release publishing...
    
    NX PUBLISHING:
    
    - ecom-domain: 0.1.0
    
    Note: The "release" command is not connected to any git remote.
    Run "nx connect" to enable remote caching and distribution for your Nx Workspace!
    
    • You should see ecom-domain incremented (e.g., from 0.0.1 to 0.1.0) because of the feat: commit. Other projects (apps) might not increment if they don’t have their own package.json for explicit versioning or if no relevant commits for them.

5.3. Executing a Real Release

  1. Run the Nx Release command:
    nx release
    
    Expected Output (creates release commit, tag, and potentially GitHub Release):
    NX Performing release versioning...
    NX   These projects will be versioned:
    - ecom-domain: 0.0.1 -> 0.1.0 (minor)
    
    NX Performing release changelog generation...
    NX   For project ecom-domain, the changelog will include:
        - feat(ecom-domain): Add imageUrl to Product interface
    
    NX Performing release git operations...
    NX Creating commit: chore(ecom-domain): release ecom-domain 0.1.0
    NX Creating tag: ecom-domain@0.1.0
    NX Performing release publishing...
    
    NX PUBLISHING:
    
    - ecom-domain: 0.1.0
    
    NX Done with release!
    
    • Check your Git history: you’ll see a new commit like chore(ecom-domain): release ecom-domain 0.1.0.
    • Check your Git tags: git tag should show ecom-domain@0.1.0.
    • If you have GitHub integration set up for Nx Cloud (and pushed the changes), Nx Cloud might also trigger a GitHub Release.

Key Learnings for Nx Release:

  • Conventional Commits: Adhering to conventional commit messages (feat:, fix:, chore:) is crucial for automatic versioning and changelog generation.
  • Independent vs. Unified: The projects array in release config determines which projects participate and how.
  • Automation: Nx automates the tedious steps of version bumping, tag creation, and changelog generation, reducing human error.
  • CI/CD Integration: This nx release command is typically run as a step in your CI/CD pipeline, often on your main branch after a successful merge.

6. Monorepo Health and Performance Tuning

Maintaining a fast and healthy monorepo, especially as it scales, requires continuous attention to performance and architectural discipline.

6.1. Deep Dive into Task Inputs & Outputs

Nx’s caching and affected system depend heavily on knowing what inputs a task uses and what outputs it produces. Fine-tuning these can yield massive performance gains.

  1. Inspect targetDefaults in nx.json:

    // nx.json (partial)
    {
      "targetDefaults": {
        "build": {
          "cache": true,
          "inputs": ["production", "^production"], // All production files + production files of dependencies
          "outputs": ["{workspaceRoot}/dist/{projectRoot}"] // Output to dist/projectRoot
        },
        "test": {
          "cache": true,
          "inputs": ["default", "^production", "{workspaceRoot}/jest.preset.js"]
        },
        "lint": {
          "cache": true,
          "inputs": ["default", "{workspaceRoot}/.eslintrc.json"]
        }
      }
    }
    
    • inputs: Defines what files/globs contribute to the hash calculation for a task. If these inputs don’t change, the task result is retrieved from cache.
      • production: A named input defined at the root of nx.json.
      • ^production: The production inputs of the project’s dependencies. This is critical for cache invalidation.
      • {workspaceRoot}/jest.preset.js: Specific file inputs.
    • outputs: Defines where the results of a task are stored in the cache. Nx will copy these files to the cache on a hit and restore them on a miss.
  2. Customizing Inputs for Specific Targets/Projects: You can override targetDefaults at the project level in project.json. For example, if a deploy target only needs dist files and certain configuration, you’d specify minimal inputs.

    • Example: deploy target inputs:
      // apps/my-app/project.json (partial)
      {
        "targets": {
          "deploy": {
            "executor": "nx:run-commands",
            "options": {
              "command": "aws s3 sync dist/apps/my-app s3://my-app-bucket",
              "cwd": "{workspaceRoot}"
            },
            "inputs": ["{projectRoot}/dist/**", "{projectRoot}/aws-config.json"], // Only depends on build output and specific config
            "dependsOn": ["build"] // Make sure build runs first
          }
        }
      }
      
    • Exercise:
      1. In libs/design-system/atoms/my-button/project.json, remove outputs: [] from the bump-version target.
      2. Run nx bump-version design-system-atoms-my-button --increment=patch.
      3. Run the command again. It should be a cache hit because the inputs (including the package.json and src/version.ts after the first run) did not change based on what Nx monitors by default for a run-commands executor (if you were to wrap this executor). Since bump-version directly modifies source files, Nx will still re-run it unless you explicitly define inputs that don’t change for the executor itself or if Nx detects a change in the inputs that would result in the same output. This is a subtle point. For a custom executor like bump-version, Nx re-runs it every time if it modifies source code directly unless its schema options or dependent files outside its own modified outputs change.
      4. Correct approach: for tasks that modify source code, caching is tricky. Usually, bump-version would be part of a release process that ensures a clean state.

6.2. Test Splitting Techniques

Long-running test suites are a major CI bottleneck. Nx integrates with testing frameworks to provide techniques for splitting tests for parallel execution.

  1. Built-in Test Sharding (Jest/Playwright): You can use the native sharding capabilities of Jest or Playwright directly via Nx.

    • Example (Jest): To split tests into 3 shards:

      nx test my-react-app --shard=1/3
      nx test my-react-app --shard=2/3
      nx test my-react-app --shard=3/3
      
    • Example (Playwright E2E - if configured):

      nx e2e my-app-e2e --shard=1/3
      
    • Integration with CI: In your GitHub Actions, you’d set up a matrix job to run these shards in parallel across multiple runners.

      # .github/workflows/ci.yml (partial, hypothetical for a test job)
      jobs:
        test-sharded:
          runs-on: ubuntu-latest
          strategy:
            matrix:
              shard: [1, 2, 3] # Create 3 parallel jobs
          steps:
            # ... checkout, setup-node, npm ci, nx-set-shas ...
            - name: Run Jest tests shard ${{ matrix.shard }}
              run: npx nx affected --target=test -- --shard=${{ matrix.shard }}/3 # Run only affected tests for this shard
      
  2. Nx Atomizer (per-file splitting): For even more granular control, Nx Atomizer can split tasks per file, providing more parallelism and better insights into flaky tests, especially with Nx Cloud. This requires specific plugin configurations (e.g., @nx/cypress/plugin, @nx/playwright/plugin, @nx/jest/plugin with ciTargetName).

    • nx.json (example for Atomizer configuration):
      // nx.json (add to "plugins" array)
      {
        "plugins": [
          {
            "plugin": "@nx/jest/plugin",
            "options": {
              "targetName": "test",
              "ciTargetName": "test-ci" // This enables Atomizer for test-ci
            }
          }
        ]
      }
      
    • Then, in CI, you’d run nx affected -t test-ci and Nx Cloud would automatically distribute the individual test files across agents.
  3. Manual E2E Project Splitting: For very large applications, splitting E2E projects into smaller, scope-specific projects and using implicitDependencies with dependsOn can ensure only relevant E2E suites run.

    • Concept: Instead of app-e2e testing everything, have feature-a-e2e and feature-b-e2e. These would have an implicitDependencies on their respective feature libraries and use dependsOn to ensure the main app builds before their tests run. This is a more involved refactoring but provides maximum efficiency.

6.3. Analyzing the Dependency Graph and Project Metrics

Understanding your monorepo’s architecture and performance hotspots is key.

  1. nx graph (--affected, --groupByFolder, --focus):

    • nx graph: Interactive visualization.
    • nx graph --affected: Visualize only affected projects.
    • nx graph --groupByFolder: Organize nodes by folder structure.
    • nx graph --focus=admin-shell: Show admin-shell and its dependencies/dependents.
    • nx graph --exclude=libs/design-system/**: Exclude certain paths.
  2. Nx Console (VS Code/IntelliJ): Provides an intuitive UI for the graph, showing inputs, outputs, and affected projects.

  3. Nx Cloud Build Details: Every CI run on Nx Cloud provides a detailed build report, showing task durations, cache hits/misses, and bottlenecks. This is invaluable for identifying areas for optimization.

Best Practices for Performance:

  • Small, Focused Libraries: Keep libraries small and cohesive to minimize their “affected” footprint.
  • Tree-Shakable Code: Ensure your libraries export only what’s needed, allowing bundlers to remove unused code.
  • Strict Module Boundaries: Prevent unintentional dependencies that can lead to unnecessary rebuilds.
  • Optimize Task Configuration: Be precise with inputs and outputs for all targets.
  • Leverage Nx Cloud: For team environments, remote caching and DTE are game-changers.
  • Regular Migrations: Stay updated with nx migrate latest to benefit from Nx’s continuous performance improvements.

7. Integrating with Non-JavaScript Technologies (Polyglot Monorepos)

Nx is not limited to JavaScript/TypeScript. It has a growing ecosystem of plugins for other languages and frameworks, allowing you to manage polyglot monorepos. This is particularly relevant for enterprises with diverse tech stacks.

We’ll briefly explore how to integrate a Java Spring Boot application.

Prerequisites:

  • Java Development Kit (JDK) 17+ installed.
  • Maven or Gradle installed.

Project Setup (continuing from previous section in advanced-nx-ws):

  1. Install the @nx/gradle plugin (or @nx/maven):

    npm install -D @nx/gradle
    
    • @nx/gradle provides integration for Java/Kotlin/Groovy projects using Gradle.
  2. Generate a new Spring Boot application:

    nx g @nx/gradle:application my-java-api --framework=spring-boot --project-name=my-java-api
    
    • @nx/gradle:application: The generator for Gradle applications.
    • my-java-api: Our Java API project name.
    • --framework=spring-boot: Specifies a Spring Boot application.

    Expected Output (creates apps/my-java-api with Maven/Gradle project structure, updates nx.json):

    NX Generating @nx/gradle:application
    
    CREATE apps/my-java-api/.gitignore
    CREATE apps/my-java-api/build.gradle
    ... (many Java/Gradle specific files)
    UPDATE nx.json
    NX Successfully ran generator @nx/gradle:application for my-java-api
    
    • Nx now treats your my-java-api as a first-class project, just like your React apps.
  3. Run Java tasks via Nx:

    • Build the Java app:
      nx build my-java-api
      
      Expected Output (Gradle/Maven build logs):
      NX Running target build for project my-java-api
      
      > Task :my-java-api:jar
      > Task :my-java-api:assemble
      > Task :my-java-api:compileJava
      ... (Gradle/Maven build output) ...
      > Task :my-java-api:build
      
      BUILD SUCCESSFUL in Xs
      NX Ran target build for project my-java-api
      
    • Serve the Java app:
      nx serve my-java-api
      
      • This will typically start the Spring Boot application.
  4. Observe Polyglot in nx graph:

    nx graph
    
    • You’ll see my-java-api alongside your React applications. Nx’s graph understands dependencies across different language ecosystems (e.g., if a React app makes API calls to my-java-api, you could model this dependency in nx.json).

Key Learning for Polyglot:

  • Nx provides a unified CLI and build system, even for diverse language stacks.
  • Benefits like affected commands, caching (configured for Gradle/Maven), and the dependency graph extend to non-JS projects.
  • This enables true enterprise monorepos managing all codebases under one roof.

8. Taking Your Nx Monorepo to Production: Deployment Strategies

Deploying a monorepo introduces unique considerations. The goal is often independent deployability – deploying only what has changed, without redeploying the entire monorepo. This saves time, resources, and minimizes risk.

We’ll cover general strategies and provide concrete examples for common cloud providers and CI tools.

8.1. Core Principles of Monorepo Deployment

  1. Independent Deployability: The golden rule. If only product-mfe changed, only product-mfe should be built and deployed. The host and other micro-frontends remain untouched.
    • Nx’s affected commands are foundational for this.
  2. Build Artifacts: Each deployable application (frontend, backend, microservice) needs to produce a self-contained build artifact (e.g., Docker image, static dist folder, JAR file).
  3. Environment Configuration: Externalize environment-specific variables (API endpoints, database connections, secrets) using .env files, cloud secret managers, or CI/CD environment variables.
  4. Deployment Orchestration: Use CI/CD pipelines (GitHub Actions, GitLab CI, Azure DevOps, Jenkins) to automate building, testing, and deploying affected applications.
  5. Rollback Strategy: Always have a plan to quickly revert to a previous stable version in case of deployment issues.

8.2. Deployment Scenarios and Strategies

Let’s consider our advanced-nx-ws example: admin-shell (host), product-mfe, order-mfe (remotes), my-java-api (backend).

Scenario 1: Frontend (Static) Applications (e.g., admin-shell, product-mfe, order-mfe)

  • Strategy: Build to static assets, upload to an S3-like static hosting service (AWS S3 + CloudFront, Azure Blob Storage + CDN, Vercel, Netlify).
  • Key Consideration for MFEs: Remote micro-frontends (like product-mfe and order-mfe) need to be deployed to publicly accessible URLs. The host (admin-shell) will then dynamically load them from these URLs at runtime.

Example: Deploying product-mfe to AWS S3 via GitHub Actions

  1. Configure product-mfe for production build: Ensure apps/product-mfe/project.json has a build target configured for production. Nx generators typically set this up.

    // apps/product-mfe/project.json (partial)
    {
      "targets": {
        "build": {
          "executor": "@nx/rspack:rspack", // Or @nx/react:webpack
          "outputs": ["{options.outputPath}"],
          "options": {
            "outputPath": "dist/apps/product-mfe",
            "main": "apps/product-mfe/src/main.tsx",
            "tsConfig": "apps/product-mfe/tsconfig.app.json",
            "assets": ["apps/product-mfe/src/favicon.ico", "apps/product-mfe/src/assets"],
            "compiler": "babel", // Or swc, tsc
            "indexHtml": "apps/product-mfe/src/index.html"
          },
          "configurations": {
            "production": {
              "optimization": true,
              "outputHashing": "all",
              "sourceMap": false,
              "namedChunks": false,
              "extractLicenses": true,
              "vendorChunk": false,
              "outputFileName": "index.js", // For MFEs, often a single entry file
              "webpackConfig": "apps/product-mfe/rspack.config.prod.js" // Your production-specific config
            }
          }
        },
        "deploy": {
          "executor": "nx:run-commands",
          "options": {
            "command": "aws s3 sync {workspaceRoot}/dist/apps/product-mfe s3://your-product-mfe-bucket --delete",
            "cwd": "{workspaceRoot}",
            "parallel": false
          },
          "dependsOn": ["build"]
        }
      }
    }
    
    • Note: You’ll need to create a rspack.config.prod.js in apps/product-mfe for production specific Rspack config (e.g., minification, code splitting, public path for MFE).
    • Crucial for MFEs: The publicPath in your MFE’s production Rspack config needs to be its deployed URL (e.g., https://product-mfe.yourdomain.com/). This is how the host finds its remote entry.
  2. GitHub Actions Workflow (.github/workflows/deploy-frontend.yml): This workflow will only run if product-mfe or order-mfe (or the host admin-shell) are affected.

    # .github/workflows/deploy-frontend.yml
    name: Deploy Affected Frontends
    
    on:
      push:
        branches:
          - main # Deploy on pushes to main
    
    permissions:
      contents: read
      id-token: write # Required for OIDC with AWS
    
    jobs:
      detect-affected-frontends:
        runs-on: ubuntu-latest
        outputs:
          affected_apps: ${{ steps.affected.outputs.affected_apps }}
        steps:
          - uses: actions/checkout@v4
            with:
              fetch-depth: 0
    
          - uses: actions/setup-node@v4
            with:
              node-version: 20
              cache: 'npm'
    
          - run: npm ci
          - uses: nrwl/nx-set-shas@v4
    
          - name: Detect affected frontend applications
            id: affected
            run: |
              # Get affected apps of type 'app' that have 'build' target and are React-based
              AFFECTED_APPS=$(npx nx show projects --affected --type=app --with-target=build --exclude apps/my-java-api --plain | xargs -n1 basename)
              # Filter for our specific frontends
              FRONTEND_APPS=""
              for app in $AFFECTED_APPS; do
                if [[ "$app" == "admin-shell" || "$app" == "product-mfe" || "$app" == "order-mfe" ]]; then
                  FRONTEND_APPS="$FRONTEND_APPS $app"
                fi
              done
              echo "Detected affected frontends: $FRONTEND_APPS"
              # Create a JSON array for matrix strategy
              MATRIX=$(echo $FRONTEND_APPS | jq -R -s -c 'split(" ") | map(select(length > 0)) | { include: map({name: .}) }')
              echo "affected_apps=$MATRIX" >> $GITHUB_OUTPUT          
    
      deploy-app:
        needs: detect-affected-frontends
        if: ${{ needs.detect-affected-frontends.outputs.affected_apps != '{"include":[]}' }}
        runs-on: ubuntu-latest
        strategy:
          matrix: ${{ fromJson(needs.detect-affected-frontends.outputs.affected_apps) }}
        env:
          APP_NAME: ${{ matrix.name }}
          AWS_REGION: us-east-1 # Your AWS region
          # You might have different S3 buckets per MFE or environment
          S3_BUCKET_ADMIN_SHELL: your-admin-shell-bucket
          S3_BUCKET_PRODUCT_MFE: your-product-mfe-bucket
          S3_BUCKET_ORDER_MFE: your-order-mfe-bucket
        steps:
          - uses: actions/checkout@v4
            with:
              fetch-depth: 0
    
          - uses: actions/setup-node@v4
            with:
              node-version: 20
              cache: 'npm'
    
          - run: npm ci
          - uses: nrwl/nx-set-shas@v4
    
          - name: Configure AWS Credentials
            uses: aws-actions/configure-aws-credentials@v4
            with:
              role-to-assume: arn:aws:iam::YOUR_AWS_ACCOUNT_ID:role/GitHubActionsDeployRole # Create this IAM role
              aws-region: ${{ env.AWS_REGION }}
    
          - name: Build ${{ env.APP_NAME }}
            run: npx nx build ${{ env.APP_NAME }} --configuration=production
    
          - name: Deploy ${{ env.APP_NAME }} to S3
            run: |
              case "${{ env.APP_NAME }}" in
                "admin-shell")
                  BUCKET_NAME="${{ env.S3_BUCKET_ADMIN_SHELL }}"
                  ;;
                "product-mfe")
                  BUCKET_NAME="${{ env.S3_BUCKET_PRODUCT_MFE }}"
                  ;;
                "order-mfe")
                  BUCKET_NAME="${{ env.S3_BUCKET_ORDER_MFE }}"
                  ;;
                *)
                  echo "Unknown application: ${{ env.APP_NAME }}"
                  exit 1
                  ;;
              esac
              echo "Deploying ${{ env.APP_NAME }} to s3://$BUCKET_NAME"
              aws s3 sync dist/apps/${{ env.APP_NAME }} s3://$BUCKET_NAME --delete --exact-timestamps --region ${{ env.AWS_REGION }}
              aws cloudfront create-invalidation --distribution-id YOUR_CLOUDFRONT_DISTRIBUTION_ID --paths "/*" # Invalidate CDN cache          
            env:
              # Ensure these secrets are set in GitHub
              AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
              AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
    
    • AWS IAM Role (GitHubActionsDeployRole): This role needs permissions for s3:PutObject, s3:DeleteObject, s3:ListBucket, cloudfront:CreateInvalidation. It should have a trust policy allowing oidc.githubusercontent.com to assume it. (Highly recommended over long-lived access keys).
    • --delete --exact-timestamps: Ensures S3 bucket content matches the dist folder and avoids unnecessary uploads.
    • CloudFront Invalidation: Crucial for static sites behind a CDN to ensure users get the latest version.

Scenario 2: Backend Applications (e.g., my-java-api, or Node.js api-server)

  • Strategy: Build to a Docker image, push to a container registry (AWS ECR, Azure Container Registry, Docker Hub), then deploy to a container orchestration service (AWS ECS/EKS, Azure AKS, Kubernetes).
  • Key Consideration: Each backend app will be a separate Docker image and potentially a separate service in your orchestration system.

Example: Deploying my-java-api to AWS ECS via GitHub Actions

  1. Configure my-java-api for Docker build: You’ll need a Dockerfile in apps/my-java-api.

    # apps/my-java-api/Dockerfile
    FROM openjdk:17-jdk-slim
    WORKDIR /app
    COPY build/libs/*.jar app.jar # Or target/*.jar for Maven
    ENTRYPOINT ["java","-jar","app.jar"]
    
    • Modify apps/my-java-api/project.json to include a docker-build target (you might create a custom executor or run-commands for this).

      // apps/my-java-api/project.json (partial)
      {
        "targets": {
          "build": { /* ... */ },
          "docker-build": {
            "executor": "nx:run-commands",
            "options": {
              "command": "docker build -f apps/my-java-api/Dockerfile -t your-ecr-repo/my-java-api:{tag} .",
              "cwd": "{workspaceRoot}"
            },
            "dependsOn": ["build"]
          },
          "deploy": {
            "executor": "nx:run-commands",
            "options": {
              "command": "aws ecs update-service --cluster your-ecs-cluster --service my-java-api-service --force-new-deployment --task-definition my-java-api-task-definition",
              "cwd": "{workspaceRoot}"
            },
            "dependsOn": ["docker-build"]
          }
        }
      }
      
  2. GitHub Actions Workflow (.github/workflows/deploy-backend.yml): This workflow will build and deploy my-java-api when affected.

    # .github/workflows/deploy-backend.yml
    name: Deploy Affected Backend (Java)
    
    on:
      push:
        branches:
          - main
    
    permissions:
      contents: read
      id-token: write
    
    env:
      AWS_REGION: us-east-1
      AWS_ACCOUNT_ID: YOUR_AWS_ACCOUNT_ID
      ECR_REPOSITORY: your-ecr-repo/my-java-api
      ECS_CLUSTER_NAME: your-ecs-cluster
      ECS_SERVICE_NAME: my-java-api-service
      ECS_TASK_DEFINITION_FAMILY: my-java-api-task-definition
    
    jobs:
      detect-affected-backend:
        runs-on: ubuntu-latest
        outputs:
          is_affected: ${{ steps.affected.outputs.is_affected }}
        steps:
          - uses: actions/checkout@v4
            with:
              fetch-depth: 0
    
          - uses: actions/setup-node@v4
            with:
              node-version: 20
              cache: 'npm'
    
          - run: npm ci
          - uses: nrwl/nx-set-shas@v4
    
          - name: Detect if my-java-api is affected
            id: affected
            run: |
              if npx nx affected --target=build --base=HEAD~1 --head=HEAD --select=projects:my-java-api --plain; then
                echo "my-java-api is affected."
                echo "is_affected=true" >> $GITHUB_OUTPUT
              else
                echo "my-java-api is NOT affected."
                echo "is_affected=false" >> $GITHUB_OUTPUT
              fi          
    
      build-and-deploy:
        needs: detect-affected-backend
        if: ${{ needs.detect-affected-backend.outputs.is_affected == 'true' }}
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v4
            with:
              fetch-depth: 0
    
          - uses: actions/setup-node@v4
            with:
              node-version: 20
              cache: 'npm'
    
          - run: npm ci # For Nx commands
          - uses: nrwl/nx-set-shas@v4
    
          - name: Set up JDK 17
            uses: actions/setup-java@v4
            with:
              distribution: 'temurin'
              java-version: '17'
              cache: 'gradle' # or 'maven'
    
          - name: Configure AWS Credentials
            uses: aws-actions/configure-aws-credentials@v4
            with:
              role-to-assume: arn:aws:iam::${{ env.AWS_ACCOUNT_ID }}:role/GitHubActionsDeployRole # Same IAM role
              aws-region: ${{ env.AWS_REGION }}
    
          - name: Login to Amazon ECR
            id: login-ecr
            run: |
              aws ecr get-login-password --region ${{ env.AWS_REGION }} | docker login --username AWS --password-stdin ${{ env.AWS_ACCOUNT_ID }}.dkr.ecr.${{ env.AWS_REGION }}.amazonaws.com          
    
          - name: Build and Push Docker image
            id: docker_build
            env:
              IMAGE_TAG: ${{ github.sha }} # Use commit SHA as image tag for uniqueness
            run: |
              npx nx build my-java-api # Build the JAR first
              docker build -t ${{ env.ECR_REPOSITORY }}:${{ env.IMAGE_TAG }} -t ${{ env.ECR_REPOSITORY }}:latest apps/my-java-api
              docker push ${{ env.ECR_REPOSITORY }}:${{ env.IMAGE_TAG }}
              docker push ${{ env.ECR_REPOSITORY }}:latest          
    
          - name: Fill in the new image ID in the Amazon ECS task definition
            id: render-task-def
            uses: aws-actions/amazon-ecs-render-task-definition@v1
            with:
              task-definition: .github/workflows/task-definition.json # Create this file
              container-name: my-java-api-container # Name of your container in task def
              image: ${{ env.ECR_REPOSITORY }}:${{ github.sha }}
    
          - name: Deploy Amazon ECS task definition
            uses: aws-actions/amazon-ecs-deploy-task-definition@v1
            with:
              task-definition: ${{ steps.render-task-def.outputs.task-definition }}
              service: ${{ env.ECS_SERVICE_NAME }}
              cluster: ${{ env.ECS_CLUSTER_NAME }}
              wait-for-service-stability: true
    
    • task-definition.json (example, needs to be created in .github/workflows/):
      # .github/workflows/task-definition.json
      {
        "family": "my-java-api-task-definition",
        "networkMode": "awsvpc",
        "cpu": "256",
        "memory": "512",
        "containerDefinitions": [
          {
            "name": "my-java-api-container",
            "image": "YOUR_AWS_ACCOUNT_ID.dkr.ecr.us-east-1.amazonaws.com/your-ecr-repo/my-java-api:latest",
            "portMappings": [
              {
                "containerPort": 8080,
                "protocol": "tcp"
              }
            ],
            "logConfiguration": {
              "logDriver": "awslogs",
              "options": {
                "awslogs-group": "/ecs/my-java-api",
                "awslogs-region": "us-east-1",
                "awslogs-stream-prefix": "ecs"
              }
            }
          }
        ],
        "requiresCompatibilities": ["FARGATE"],
        "executionRoleArn": "arn:aws:iam::YOUR_AWS_ACCOUNT_ID:role/ecsTaskExecutionRole"
        // Add taskRoleArn if your app needs AWS permissions
      }
      
    • OIDC and IAM Roles: Use AWS OIDC for secure, credential-less authentication from GitHub Actions to AWS. Create appropriate IAM roles (e.g., GitHubActionsDeployRole, ecsTaskExecutionRole).
    • Image Tagging: Using github.sha as an image tag ensures unique, immutable deployments.
    • ECS Task Definition & Service: The workflow updates an existing ECS service with a new task definition pointing to the newly built Docker image.

8.3. Dealing with Deployment Errors and Best Practices

  • Rollback Procedures:
    • For static sites: Revert S3 bucket to a previous version, or point CloudFront to a previous S3 object version.
    • For containerized apps: Revert ECS service to a previous task definition revision, or deploy a previous Docker image tag.
  • Monitoring & Alerting: Integrate with cloud monitoring tools (AWS CloudWatch, Azure Monitor) for application metrics, logs, and error alerts.
  • Health Checks: Configure robust health checks (Liveness/Readiness probes) in your container orchestration.
  • Environment Variables: Store secrets in AWS Secrets Manager, Azure Key Vault, or similar. Inject non-sensitive environment variables via your CI/CD or deployment system.
  • Testing: Comprehensive unit, integration, and E2E tests are your first line of defense against deployment errors. nx affected --target=e2e is crucial.
  • Canary Deployments/Blue-Green: For critical applications, consider advanced deployment strategies that gradually roll out new versions to a small subset of users or run new and old versions side-by-side.
  • Documentation: Maintain clear documentation of your deployment processes, environment configurations, and rollback steps.

Monorepo Deployment Advantages with Nx:

  • Targeted Deployments: nx affected allows you to build and deploy only the applications that have changed, drastically reducing deployment times and risk compared to rebuilding everything.
  • Consistent Builds: Nx’s caching ensures that nx build always produces the same artifact for the same inputs, regardless of where it’s run (local dev or CI).
  • Shared Infrastructure Code: You can have infrastructure-as-code libraries (Terraform, CloudFormation, Pulumi) within your monorepo, managed by Nx, making infrastructure changes consistent and versioned alongside your application code.

9. Bonus Section: Further Learning and Resources

You’ve now covered extensive advanced topics in Nx Workspace. The journey doesn’t end here; the Nx ecosystem is constantly evolving.

  • Official Nx Documentation: Continually updated for the latest features. It’s your primary source.
    • https://nx.dev/
    • Pay special attention to the “Recipes” and “Reference” sections for deep dives.
  • Nx Cloud Documentation: Essential for advanced CI/CD.
  • Nrwl YouTube Channel: Look for advanced workshops, talks, and deep dives into specific features like Module Federation.

Blogs and Articles:

  • Nx Blog: https://nx.dev/blog - Stay up-to-date with bleeding-edge features like Self-Healing CI and new plugin integrations (e.g., recent articles on Nx and AI, Java/Docker integration).
  • Medium/Dev.to: Search for “Nx Module Federation,” “Nx CI/CD,” “Custom Nx Plugin” + “2025” or “Latest” to find community-driven advanced tutorials.

Community & Support:

  • Nx Discord Server: https://go.nx.dev/community - The official Discord is a fantastic place for advanced discussions, troubleshooting, and direct interaction with the Nx team and experts.
  • GitHub Issues: For reporting bugs or requesting features, the Nx GitHub repository is your go-to.

Next Frontiers:

  • Nx and AI Integration: As seen in recent Nx blog posts, Nx is integrating deeply with AI assistants for smarter development workflows, self-healing CI, and enhanced code generation. Explore these features as they mature.
  • Advanced Module Federation Patterns: Investigate dynamic module loading strategies, versioning for remotes, and overcoming common MFE challenges.
  • Complex Monorepo Refactoring: Learn strategies for migrating large legacy codebases into an Nx monorepo and breaking down existing monoliths.
  • Security in Monorepos: Dive into dependency vulnerability scanning, secret management, and access control within a monorepo context.
  • Enterprise Nx Cloud Features: Explore advanced DTE configurations, analytics, and custom dashboards offered by Nx Cloud for large organizations.

By actively engaging with these resources and continuing to experiment, you will solidify your expertise and become an invaluable asset in any development team leveraging Nx Workspace. The journey of mastering a build system like Nx is continuous, but with these advanced tools and concepts, you are well-equipped to tackle the most demanding projects. Happy building and deploying!

For more Advance topics follow here -> Next Frontiers in Nx Workspace: An Advanced Developer’s Guide