Skip to content

Latest commit

 

History

History

README.md

Monorepository

Different url per application under the same domain with module federation

When your applications are live under the same domain but at different URLs, for example https://website.com/home and https://website.com/blog, then you will have to use a reverse proxy at some point in your stack to be able to route the traffic to your different frontend applications. You can still use a monorepository to serve your different frontend applications.

In fact it presents many advantages.

The simplest setup you can use to map incoming requests is a dedicated reverse proxy such as nginx but you can use any reverse proxy at any point in your stack.

In this example, we have 2 folders homepage and blog under apps which you can see as 2 different repositories you can host and deploy completely independently and connect their exposed endpoint to your reverse proxy.

When to use ?

You have distinct apps serving different purposes and you want different applications and/or teams to align and share their development setup and practices to encourage reusability and reduce overall efforts on developer experience and dependencies management.

You also want to share runtime dependencies between your apps that you can update without redeploying your apps.

Consequences of this setup:

Pros:

  1. Streamlined development with shared opininated configurations (Typescript, formatting, linting etc.) that allow developers to focus on delivering business value.
  2. You no longer have to synchronize shared dependencies releases and updates accross many scattered repositories.
  3. Cross team contributions are much easier.
  4. One (possibly virtual) team can focus on operational work (dependency management, security maitenance, local developer experience, CI/CD, devops, etc.), and all teams will benefit from it.
  5. You can release hotfixes and new features for your runtime dependencies without having to synchronize and redeploy all your applications.

Cons:

  1. You have to invest in the initial setup for example to setup monorepository tooling.
  2. You have to invest into a collaboration model and a proper code architecture for the monorepository (which is a benefit in disguise).
  3. You have to monitor your runtime dependencies like you would for regular applications.

Setup

I am using the default workspace setup from pnpm with a pnpm-workspace.yaml configuration as follows:

packages:
  - "apps/*"

homepage and blog folders under apps each contain a simple Typescript app built with Vite.

homepage and blog apps are setup as module federation hosts and served at predefined ports for our reverse proxy. Example Vite configuration for homepage:

import { defineConfig } from "vite";
import { federation } from "@module-federation/vite";

export default defineConfig({
  server: {
    port: 3000,
  },
  base: "/homepage",
  plugins: [
    federation({
      name: "homepage",
      remotes: {
        banner: {
          type: "module",
          name: "banner",
          entry: "http://localhost:2000/banner.js",
          entryGlobalName: "remote-banner",
        },
        "web-vitals-reporter": {
          type: "module",
          name: "web-vitals-reporter",
          entry: "http://localhost:2001/web-vitals-reporter.js",
          entryGlobalName: "remote-web-vitals-reporter",
        },
      },
    }),
  ],
});

remote-banner and remote-web-vitals-reporter simulate how shared runtime dependencies apps would run and be injected at runtime through module federation. They do not have to know anything about their host applications. Example Vite configuration for the banner:

import { defineConfig } from "vite";
import { federation } from "@module-federation/vite";

export default defineConfig({
  server: {
    origin: "http://localhost:2000",
    port: 2000,
  },
  base: "http://localhost:2000",
  plugins: [
    federation({
      name: "banner",
      filename: "banner.js",
      exposes: {
        ".": "./src/main.ts",
      },
    }),
  ],
});

Update your tsconfig.json configuration with the paths of the remote runtime dependencies:

"paths": {
  "web-vitals-reporter": ["./apps/remote-web-vitals-reporter/src/main.ts"],
  "banner": ["./apps/remote-banner/src/main.ts"]
}

Finally, for the reverse proxy, as a demo, I am using nginx. We extend the default nginx configuration with a local reverse-proxy.conf file:

server {
    listen 8080;

      location /home {
        rewrite ^ /homepage break;
        proxy_pass http://localhost:3000;
    }

    location /homepage {
        proxy_pass http://localhost:3000;
    }

    location /blog {
        proxy_pass http://localhost:4000;
    }
}

I think I've hit a weird bug with Windows Subsystem for Linux and /home with Vite module federation so that's why in this example I am redirecting /home to /homepage but it should not be necessary with a native unix systems.

Note: you can use the reverse proxy of choice and it can be deployed where you want in your stack.

Demo

  1. At the base folder, install dependencies:
pnpm install
  1. Open 5 terminal windows to install dependencies and run applications as well as nginx:

  2. Homepage host app:

cd ./apps/homepage && pnpm run dev
  1. Blog host app:
cd ./apps/blog && pnpm run dev
  1. Shared remote banner app:
cd ./apps/remote-banner && pnpm run dev
  1. Shared remote web-vitals-reporter app:
cd ./apps/remote-web-vitals-reporter && pnpm run dev
  1. Start nginx:
[sudo] nginx -c %ABSOLUTE_PATH_TO_THIS_FOLDER%/reverse-proxy.conf

You can now navigate to http://localhost:8080/home and http://localhost:8080/blog to access your frontend applications. (Do not directly go to localhost:3000 or localhost:4000 otherwise navigation won't work.)

If you open the console and reload the page, you will see the web vitals reporting coming from the remote web-vitals-reporter module.

  1. Stop nginx with
[sudo] nginx -c %ABSOLUTE_PATH_TO_THIS_FOLDER%/reverse-proxy.conf -s quit