Shell routing, remote routing, and hybrid models for Module Federation microfrontends, and how React Bridge changes the default.
Every microfrontend setup eventually asks the same question: who owns the browser URL?
Get it wrong and you get double mounts, broken back buttons, duplicated layouts, or remotes that only work when embedded, not when bookmarked.
There is no universal best answer. There are three common models. The right one depends on how independent your teams (and remotes) really are.
Shell-owned Shell owns every route. Remotes fill slots.
Remote-owned Shell mounts a prefix. Remote owns the subtree.
Hybrid Shell owns the top. Remotes own deep prefixes.The host owns the router. Remotes render into route slots. They do not register top-level routes.
/ -> Shell + Dashboard remote
/orders -> Shell + Orders list remote
/orders/:id -> Shell + Order detail remoteWorks when you want one product surface: shared nav, auth guards, analytics, deep links.
Cost: the shell team becomes the coordinator for new top-level routes.
Each remote ships its own router subtree. The shell mounts it at a prefix and gets out of the way.
/billing/* -> Billing remote owns everything under /billing
/support/* -> Support remote owns everything under /supportWorks when remotes were standalone apps before federation, or teams need full autonomy over nested navigation.
Cost: inconsistent URL patterns, harder global guards, more trailing-slash edge cases.
Shell owns product-wide routes. Remotes own deep subtrees under delegated prefixes.
/ -> shell
/orders -> shell + orders list
/orders/:id/* -> orders remote owns the rest
/admin/* -> admin remote owns subtreeThis is the pragmatic enterprise pattern: constrain what must be consistent, delegate what can vary.
Routing choice tracks how you cut the remote, not which bundler you use.
| Cut | Typical routing | Tooling |
|---|---|---|
| Granular React components | Shell-owned | Expose widgets; shell wires routes |
| Whole apps (application-level modules) | Remote-owned or hybrid | React Bridge |
With component-level remotes, shell-owned routing is natural. The host already owns composition, so it should own the URL map too.
With application-level remotes (a remote that is basically a full app with its own router), prefer remote-owned or hybrid. That is what @module-federation/bridge-react is for: export an app with createBridgeComponent, load it with createRemoteAppComponent, and let Bridge coordinate basename + router context. See also exporting and loading apps. Modern.js wires the same Bridge APIs through its Module Federation integration.
// Host: mount an application remote under a prefix
<Route path="/billing/*" element={<BillingApp basename="/billing" />} />Inside the remote, Bridge can inject basename so nested routes stay under /billing without fighting the shell history.
BrowserRouter.| Need | Likely model |
|---|---|
| One cohesive product | Shell-owned |
| Maximum team autonomy / former standalone apps | Remote-owned |
| Shared top nav + deep team freedom | Hybrid |
My default: start shell-owned. It forces clarity early (one URL map, one layout, one auth story).
When a team hits real limits (large, fast-moving subtree), delegate a prefix and move to hybrid. Full remote-owned routing is powerful, but it is also the easiest way to accidentally build four separate products that happen to share a domain.
Routing is not an implementation detail. It is the contract users feel every time they click a link.
Inspired by Zach Lloyd's software factory posts — a DIY take on squeezing more out of subscriptions you already pay for, with GitHub, Cursor, OpenCode, or whatever CLI fits the job.
Field notes on Cursor, Codex, Copilot, T3 Code, and the CI/review gap that still makes parallel agents feel like unpaid work.