Why Use Single-SPA and Nx for Microfrontends?

Single-SPA and Nx each bring unique strengths to the microfrontend architecture:

  • Single-SPA: This framework enables multiple frontend applications (microfrontends) to coexist on the same page, handling application lifecycles, routing, and communication between microfrontends. Single-SPA allows different frameworks (such as React, Angular, and Vue) to coexist.
  • Nx: Nx is a powerful monorepo management tool with built-in support for modular development, dependency management, and build optimization. Nx provides a streamlined way to manage multiple applications and libraries in a single repository.

Combining these tools allows organizations to scale frontend development, increase team productivity, and support a variety of frontend technologies within a single monorepo.

Setting Up a Monorepo with Nx

To get started, we’ll first set up an Nx workspace for our monorepo. The workspace will contain both the root configuration (which handles global settings and routing) and individual microfrontends.

Step 1: Install Nx

If Nx isn’t already installed, you can add it globally with the following command:

npm install -g nx

Step 2: Initialize an Nx Workspace

Create a new Nx workspace by running:

npx create-nx-workspace@latest microfrontend-monorepo

During setup, choose an empty workspace (with no preset) to give us complete control over the structure. Once created, your workspace will look like this:

/apps
/libs
/tools

Setting Up Single-SPA in the Nx Monorepo

With the Nx workspace set up, we can now add Single-SPA and configure it for microfrontend integration.

Step 3: Install Single-SPA Dependencies

To enable Single-SPA, install the necessary packages:

npm install single-spa single-spa-layout

These packages provide the base framework for handling microfrontends and layouts within Single-SPA. You’ll also need a bundler like Webpack, which we’ll configure for each microfrontend.

Step 4: Create the Root Config

The root config handles routing between microfrontends and controls which applications are loaded based on the URL. To create the root config, generate a new application in the apps directory:

nx generate @nrwl/react:app root-config

In this directory, create an index.ejs file to serve as the main HTML template:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Microfrontend App</title>
</head>
<body>
  <div id="single-spa-root"></div>
  <script src="/apps/root-config/main.js"></script>
</body>
</html>

Next, create a single-spa-config.js file in root-config to manage routing and lifecycle events for the microfrontends:

import { registerApplication, start } from 'single-spa';

registerApplication({
  name: '@project/app1',
  app: () => System.import('@project/app1'),
  activeWhen: ['/app1']
});

registerApplication({
  name: '@project/app2',
  app: () => System.import('@project/app2'),
  activeWhen: ['/app2']
});

start();

This configuration registers app1 and app2 to load when users navigate to /app1 and /app2, respectively.

Creating Microfrontends in Nx

Now that the root config is set up, we can create individual microfrontend applications within the Nx monorepo. Each microfrontend will be a standalone application registered with Single-SPA.

Step 5: Generate Microfrontend Applications

Create two microfrontend applications in the apps directory:

nx generate @nrwl/react:app app1
nx generate @nrwl/react:app app2

These commands generate basic Nx applications. Each app has its own main.js file, which serves as the entry point.

Step 6: Configure Single-SPA Lifecycle in Each Microfrontend

In each microfrontend’s main.js, modify the code to use Single-SPA’s lifecycle methods:

import { registerApplication, start } from 'single-spa';

function mount() {
  ReactDOM.render(<App />, document.getElementById('root'));
}

function unmount() {
  ReactDOM.unmountComponentAtNode(document.getElementById('root'));
}

export const bootstrap = [() => Promise.resolve()];
export const mount = [mount];
export const unmount = [unmount];

start();

The mount and unmount functions control rendering of the microfrontend, while Single-SPA handles loading and unloading based on the root config.

Setting Up Webpack for Microfrontends

Each microfrontend needs to be bundled in a way that allows it to be dynamically loaded by Single-SPA. Webpack is commonly used for this purpose.

Step 7: Configure Webpack for Each Microfrontend

Create or update the webpack.config.js file in each microfrontend’s directory:

const HtmlWebpackPlugin = require('html-webpack-plugin');
const { merge } = require('webpack-merge');
const path = require('path');

module.exports = merge(commonConfig, {
  output: {
    filename: '[name].js',
    libraryTarget: 'system'
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, 'index.html')
    })
  ],
  externals: ['react', 'react-dom']
});

This configuration sets libraryTarget to system, which enables compatibility with Single-SPA’s SystemJS module loader. React and ReactDOM are marked as external dependencies so they aren’t bundled with each microfrontend.

Configuring Routing with Single-SPA Layout

Single-SPA Layout provides a way to manage routing between microfrontends, creating a seamless user experience across applications. To enable routing, update single-spa-config.js in root-config:

import { constructRoutes, constructApplications, constructLayoutEngine } from 'single-spa-layout';
import microfrontendLayout from './microfrontend-layout.html';

const routes = constructRoutes(microfrontendLayout);
const applications = constructApplications({ routes });
const layoutEngine = constructLayoutEngine({ routes });

start();

This setup uses a microfrontend-layout.html file to define routes, which helps manage navigation between different microfrontends.

Best Practices for Managing Microfrontends in Nx

1. Use Shared Libraries

Create shared libraries in the /libs directory to store components, utilities, or services that can be reused across microfrontends. This reduces code duplication and ensures consistency across applications.

2. Keep Microfrontends Independent

One of the advantages of microfrontends is that they can be developed and deployed independently. Avoid direct dependencies between microfrontends and instead use shared services or events for communication.

3. Optimize Build and Deployment

As you add more microfrontends, consider using techniques like code splitting and lazy loading to optimize performance. Nx’s caching and incremental builds can also help speed up the build process.

Testing and Debugging Single-SPA Microfrontends in Nx

Testing and debugging are essential for a stable microfrontend architecture. Here are a few ways to test and debug Single-SPA applications in an Nx monorepo:

Unit Testing with Jest

Each microfrontend should have its own unit tests, which can be configured using Jest within Nx. Unit tests help ensure that individual components and modules function as expected.

End-to-End Testing with Cypress

Cypress is useful for end-to-end testing across microfrontends. You can set up Cypress tests to simulate user interactions and validate that the applications load and interact properly.

Using Single-SPA Inspector for Debugging

The Single-SPA Inspector Chrome extension helps debug applications by showing information about each microfrontend, including their status and lifecycle events. It’s an invaluable tool for troubleshooting microfrontend loading and rendering issues.

Conclusion

Integrating Single-SPA in an Nx monorepo enables scalable and flexible microfrontend development. By following the steps outlined in this guide, you can set up a microfrontend architecture that leverages the strengths of both Single-SPA and Nx, supporting modular development, efficient dependency management, and independent deployment.

With Single-SPA and Nx, your team can benefit from increased productivity, flexibility, and maintainability in managing large-scale frontend applications. As you continue to develop and expand your microfrontend architecture, remember to follow best practices for modularization, shared libraries, and testing to maintain a cohesive and efficient development environment.