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.
Create a new workspace:
npx create-nx-workspace@latest advanced-nx-ws- Choose
Nonefor the stack,Yesfor Prettier,Do it laterfor CI,Nofor 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- Choose
Navigate into your workspace:
cd advanced-nx-wsInstall 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.
Generate a new Nx plugin: We’ll place our custom plugin under a
toolsdirectory, 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-pluginfolder, updatesnx.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
generatorsandexecutorsfolder insidetools/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.
Generate a new generator within our plugin:
nx g @nx/plugin:generator component-lib --project=internal-plugincomponent-lib: The name of our new generator.--project=internal-plugin: Specifies that this generator belongs to ourinternal-plugin.
Expected Output (creates
tools/internal-plugin/src/generators/component-libfolder):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-libDefine 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"] }$defaultandx-promptmake the generator interactive in the CLI or Nx Console.
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 thisTree, 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.
- Key Devkit functions:
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-runExpected Output (lots of
CREATE,UPDATElogs, 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-runRun 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.jsonandtsconfig.base.jsonfor updates. - Run
nx graphand you’ll see your newdesign-system-atoms-my-buttonlibrary.
- You’ll see the files created under
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.
Generate a new executor within our plugin:
nx g @nx/plugin:executor bump-version --project=internal-pluginbump-version: The name of our new executor.
Expected Output (creates
tools/internal-plugin/src/executors/bump-versionfolder):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-versionDefine 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"] }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.
- Dependency:
Add the custom executor to
design-system-atoms-my-button’sproject.json: We need to tell Nx that ourmy-buttonlibrary has a newbump-versiontarget 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.tsfile in yourmy-buttonlibrary:// libs/design-system/atoms/my-button/src/version.ts export const version = '0.0.0'; - Update
libs/design-system/atoms/my-button/package.jsonto 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 // ... }
- Open
Test our custom executor:
Dry run:
nx bump-version design-system-atoms-my-button --dry-run --increment=minorExpected 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-buttonRun for real:
nx bump-version design-system-atoms-my-button --increment=minorExpected 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.jsonnow has"version": "0.1.0"andlibs/design-system/atoms/my-button/src/version.tscontainsexport const version = '0.1.0';.
- Verify that
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-buttonlibrary.
Project Setup (continuing from previous section in advanced-nx-ws):
Install React and Rspack plugins for Module Federation: You should already have
@nx/reactinstalled. 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/rspackEnsure
@nx/reactis 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.
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 configureadmin-shellto consume two remote applications namedproduct-mfeandorder-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-mfeand 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, andorder-mfe. - It also configured Module Federation in their respective
module-federation.config.tsfiles.
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.
Use
design-system-atoms-my-buttoninproduct-mfe:- Open
apps/product-mfe/src/app/app.tsx. - Modify it to use our shared
Buttoncomponent 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; }
- Open
Use
design-system-atoms-my-buttoninorder-mfe:- Open
apps/order-mfe/src/app/app.tsx. - Modify it similarly to use the
Buttoncomponent 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; }
- Open
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; }
- Open
3.3. Running and Observing Module Federation
Nx makes running Module Federation locally quite elegant.
- Serve the host application:Expected Output (Nx will automatically serve
nx serve admin-shellproduct-mfeandorder-mfestatically!):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 servedproduct-mfeandorder-mfeas well, usually statically or by proxying to their own dev servers if specified withdevRemotes. - Open
http://localhost:4200in 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-buttonlibrary.
- Notice that you only ran
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-buttonlibrary), 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-shellstatically knows aboutproduct-mfeandorder-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:Follow the prompts to connect your
npx nx connectadvanced-nx-wsto Nx Cloud.
- If you haven’t connected:
4.1. The Standard CI Workflow (Pre-Nx Cloud)
Let’s first generate a basic GitHub Actions workflow file to see the starting point.
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 singleubuntu-latestrunner.
- This will create
4.2. Enhancing CI with Nx Cloud (Remote Caching & DTE)
Now, let’s modify ci.yml to leverage Nx Cloud for speed.
Modify
.github/workflows/ci.yml: We’ll uncomment and add steps for Nx Cloud’s DTE. Replace thenpx nx affected -t lint test buildline.# .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 }}Add
NX_CLOUD_ACCESS_TOKENto 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.
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.
- Observe the GitHub Actions run. You should see multiple “Agent” jobs spinning up and tasks (
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 runsnpx nx-cloud start-agentto register itself. Nx intelligently distributes thelint,test,build,e2etasks among them. nx-set-shas(or manualgitsetup): Essential fornx affectedcommands to correctly determine the base for comparison in CI.- Self-Healing CI (Optional but powerful): The
nx fix-cicommand, 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-buttonandecom-domainlibraries are marked aspublishablein theirproject.jsonfiles. (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.
Configure
nx.jsonfor independent release: Opennx.jsonand modify/add thereleasesection.// 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 thelibsfolder 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.
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
imageUrlproperty toProduct(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"
- Edit
Run the Nx Release dry run:
nx release version --dry-runExpected 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-domainincremented (e.g., from0.0.1to0.1.0) because of thefeat:commit. Other projects (apps) might not increment if they don’t have their ownpackage.jsonfor explicit versioning or if no relevant commits for them.
- You should see
5.3. Executing a Real Release
- Run the Nx Release command:Expected Output (creates release commit, tag, and potentially GitHub Release):
nx releaseNX 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 tagshould showecom-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.
- Check your Git history: you’ll see a new commit like
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
projectsarray inreleaseconfig 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 releasecommand is typically run as a step in your CI/CD pipeline, often on yourmainbranch 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.
Inspect
targetDefaultsinnx.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 ofnx.json.^production: Theproductioninputs 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.
Customizing Inputs for Specific Targets/Projects: You can override
targetDefaultsat the project level inproject.json. For example, if adeploytarget only needsdistfiles and certain configuration, you’d specify minimal inputs.- Example:
deploytarget 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:
- In
libs/design-system/atoms/my-button/project.json, removeoutputs: []from thebump-versiontarget. - Run
nx bump-version design-system-atoms-my-button --increment=patch. - Run the command again. It should be a cache hit because the inputs (including the
package.jsonandsrc/version.tsafter the first run) did not change based on what Nx monitors by default for arun-commandsexecutor (if you were to wrap this executor). Sincebump-versiondirectly 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 likebump-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. - Correct approach: for tasks that modify source code, caching is tricky. Usually,
bump-versionwould be part of areleaseprocess that ensures a clean state.
- In
- Example:
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.
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/3Example (Playwright E2E - if configured):
nx e2e my-app-e2e --shard=1/3Integration 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
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/pluginwithciTargetName).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-ciand Nx Cloud would automatically distribute the individual test files across agents.
Manual E2E Project Splitting: For very large applications, splitting E2E projects into smaller, scope-specific projects and using
implicitDependencieswithdependsOncan ensure only relevant E2E suites run.- Concept: Instead of
app-e2etesting everything, havefeature-a-e2eandfeature-b-e2e. These would have animplicitDependencieson their respective feature libraries and usedependsOnto ensure the main app builds before their tests run. This is a more involved refactoring but provides maximum efficiency.
- Concept: Instead of
6.3. Analyzing the Dependency Graph and Project Metrics
Understanding your monorepo’s architecture and performance hotspots is key.
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: Showadmin-shelland its dependencies/dependents.nx graph --exclude=libs/design-system/**: Exclude certain paths.
Nx Console (VS Code/IntelliJ): Provides an intuitive UI for the graph, showing inputs, outputs, and affected projects.
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
inputsandoutputsfor all targets. - Leverage Nx Cloud: For team environments, remote caching and DTE are game-changers.
- Regular Migrations: Stay updated with
nx migrate latestto 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):
Install the
@nx/gradleplugin (or@nx/maven):npm install -D @nx/gradle@nx/gradleprovides integration for Java/Kotlin/Groovy projects using Gradle.
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-apiwith Maven/Gradle project structure, updatesnx.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-apias a first-class project, just like your React apps.
Run Java tasks via Nx:
- Build the Java app:Expected Output (Gradle/Maven build logs):
nx build my-java-apiNX 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.
- Build the Java app:
Observe Polyglot in
nx graph:nx graph- You’ll see
my-java-apialongside your React applications. Nx’s graph understands dependencies across different language ecosystems (e.g., if a React app makes API calls tomy-java-api, you could model this dependency innx.json).
- You’ll see
Key Learning for Polyglot:
- Nx provides a unified CLI and build system, even for diverse language stacks.
- Benefits like
affectedcommands, 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
- Independent Deployability: The golden rule. If only
product-mfechanged, onlyproduct-mfeshould be built and deployed. The host and other micro-frontends remain untouched.- Nx’s
affectedcommands are foundational for this.
- Nx’s
- Build Artifacts: Each deployable application (frontend, backend, microservice) needs to produce a self-contained build artifact (e.g., Docker image, static
distfolder, JAR file). - Environment Configuration: Externalize environment-specific variables (API endpoints, database connections, secrets) using
.envfiles, cloud secret managers, or CI/CD environment variables. - Deployment Orchestration: Use CI/CD pipelines (GitHub Actions, GitLab CI, Azure DevOps, Jenkins) to automate building, testing, and deploying affected applications.
- 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-mfeandorder-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
Configure
product-mfefor production build: Ensureapps/product-mfe/project.jsonhas abuildtarget 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.jsinapps/product-mfefor production specific Rspack config (e.g., minification, code splitting, public path for MFE). - Crucial for MFEs: The
publicPathin 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.
- Note: You’ll need to create a
GitHub Actions Workflow (
.github/workflows/deploy-frontend.yml): This workflow will only run ifproduct-mfeororder-mfe(or the hostadmin-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 fors3:PutObject,s3:DeleteObject,s3:ListBucket,cloudfront:CreateInvalidation. It should have a trust policy allowingoidc.githubusercontent.comto assume it. (Highly recommended over long-lived access keys). --delete --exact-timestamps: Ensures S3 bucket content matches thedistfolder and avoids unnecessary uploads.- CloudFront Invalidation: Crucial for static sites behind a CDN to ensure users get the latest version.
- AWS IAM Role (
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
Configure
my-java-apifor Docker build: You’ll need aDockerfileinapps/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.jsonto include adocker-buildtarget (you might create a custom executor orrun-commandsfor 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"] } } }
GitHub Actions Workflow (
.github/workflows/deploy-backend.yml): This workflow will build and deploymy-java-apiwhen 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: truetask-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.shaas 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=e2eis 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 affectedallows 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 buildalways 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.
Recommended Online Courses/Tutorials:
- 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