Carlos Cifuentes
Micro-frontends · Routing

Routing across micro-frontends: route ownership, deep links and the navigation contract

The URL is the one thing every micro-frontend shares with every other. Clear rules about who owns which part of it prevent most integration problems before they appear.

By Carlos Cifuentes · Frontend architect · Updated 2026-10-02

Route ownership

The basic rule is one prefix per remote. The host owns the first URL segment and decides which remote it belongs to. The remote owns everything after it.

URLOwner
/ and /loginHost
/billing and everything belowBilling remote
/catalog and everything belowCatalog remote
Any unknown first segmentHost (not-found page)

Two remotes never share a prefix. If a screen seems to belong to two areas, pick one owner and let the other link to it.

How it looks in Angular with federation

The host declares one lazy route per remote. The remote exports its child routes and never mentions its own prefix.

// host
export const routes: Routes = [
  { path: '', component: HomeComponent },
  {
    path: 'billing',
    loadChildren: () =>
      loadRemoteModule('billing', './routes')
        .then(m => m.BILLING_ROUTES)
  },
  { path: '**', component: NotFoundComponent }
];

// billing remote
export const BILLING_ROUTES: Routes = [
  { path: '', component: InvoiceListComponent },
  { path: 'invoices/:id', component: InvoiceDetailComponent }
];

Because the remote's routes are relative, the host can mount it under another prefix without any change in the remote.

Lazy loading

A remote is downloaded the first time its prefix is visited. Until then it costs the user nothing.

Two refinements are worth considering:

  • Preloading. After the first screen is idle, the host can preload the remote the user is most likely to open next. Keep the list short. Preloading everything removes the benefit of lazy loading.
  • Loading state. The first visit to a remote includes a network round trip. Show a skeleton or progress indicator in the content area, and keep the header and menu interactive.

The effect on load times is discussed in Micro-frontend performance.

Deep links

Every screen must be reachable from a cold start. A user opens /billing/invoices/123 from an email in a new tab. This has to happen:

  1. The server returns the host's index.html for any path. This is the usual single-page application fallback, and it must cover the remote prefixes.
  2. The host starts and restores the session.
  3. The host router matches billing and loads the remote.
  4. The remote's routes match invoices/123 and the screen loads its own data.

The usual ways this breaks:

  • A screen assumes state left by a previous screen, such as a selected customer in memory. On a cold start that state does not exist. Each screen must be able to load what it needs from the URL.
  • A guard in the remote runs before the host has restored the session, and redirects to login in a loop.
  • The remote loads its assets with relative URLs, which resolve against the host's address instead of the remote's.
  • The fallback rule on the server covers / but not nested paths.

Add one deep-link test per remote to the integration suite: open an inner URL in a fresh browser session and check that the right screen appears. It catches all four problems above.

The navigation contract

Teams should agree on these rules and write them down.

  • Cross-area navigation uses URLs only. Billing links to /customers/42. It does not import a component or a route constant from the customers remote.
  • Public URLs are an API. Each remote lists the URLs that others may link to. Those stay stable or get redirects. Internal URLs can change freely.
  • Navigation goes through the router. Assigning location.href reloads the whole host and loses in-memory state.
  • Shareable state goes in the URL. Filters, tabs and pagination as query parameters survive reloads and can be sent to a colleague.
  • Each side handles its own unknown routes. The host shows a not-found page for unknown prefixes. Each remote does the same inside its prefix.

More ways for remotes to exchange information are in Communication between micro-frontends.

When remotes have their own router

With single-spa, or with remotes wrapped as web components, each remote runs its own router instance. Several routers then observe the same URL. Three extra rules apply:

  1. Each router handles only its own prefix and ignores everything else.
  2. All navigation goes through the browser History API, so every router is notified of changes.
  3. A remote that is being unmounted stops reacting to URL changes.

This model is compared in single-spa vs Module Federation.

Checklist

  1. Does every remote have exactly one prefix, and is the list written down?
  2. Do remotes use only relative routes internally?
  3. Does a deep link to an inner screen work in a fresh session?
  4. Is there a published list of public URLs per remote?
  5. Is there any location.href assignment for internal navigation? Replace it.
  6. Do unknown routes show a helpful page at both levels?

More in this section

Planning a migration or a micro-frontend platform?

In a 20-minute call you describe your situation and I tell you how I'd approach it. If it isn't worth doing yet, I'll say so.

Message me on LinkedIn