skip to content

Routing & Ownership

The shell app owns the URL, so routes have to be split among microfrontends without breaking navigation or browser history. You will map URL segments to teams and repos, which is where routing turns into an ownership question.

part ofSoftware design & architectureoverview, primer and where to startread it →
on this pageshow

questions

6

In a microfrontend shell application, what is the difference between path-based routing (e.g. example.com/checkout) and hash-based routing (e.g. example.com/#/checkout) for deciding which microfrontend to load?

level: juniorimportance: must knowfreq 70%

answer

  1. # never hits the server
  2. SPA fallback / catch-all rewrite
  3. pathname vs hash listener
  4. SEO + SSR need real paths
  5. ownership enforceable at CDN only for path routing

basics

~20 s

Path-based routing uses the real URL path and needs server/CDN config to serve the app for every path; hash-based routing puts the route after a # so the browser never asks the server for it, but it looks less clean and hurts SEO.

solid answer

~40 s

Path routing (`/checkout/cart`) treats the segment after the domain as the real address; the web server or CDN must be configured to return the shell's `index.html` for any such path (a catch-all/rewrite rule), after which client-side JS reads `window.location.pathname` to decide which microfrontend to mount. Hash routing (`/#/checkout/cart`) exploits the fact that browsers never send the fragment after `#` to the server, so any static host works with zero rewrite rules, but URLs are uglier, non-SEO-friendly, and don't work with server-side rendering per route. Most production shells (single-spa, Module Federation shells) default to path-based routing with proper server rewrites and reserve hash-based for embedded/legacy or iframe-constrained contexts.

go deeper

for a junior

Should correctly state that hash routing needs no server config while path routing does, and give the syntax difference. Doesn't need to discuss ownership or SSR implications.

for a middle

Should explain the SPA-fallback rewrite mechanism and name at least one concrete trade-off on each side (SEO/SSR for path; zero-config embeddability for hash).

for a senior

Should connect the choice to ownership enforcement — path prefixes can be routed at the CDN/edge to different origins per team; hash routing can't be enforced below the client JS layer.

for a principal

Should reason about this as a platform decision with organizational consequences: which choice lets teams deploy independently, be indexed independently, and enforce boundaries without trusting every other team's JS.

## What the shell is deciding A **microfrontend shell** (also called an app-shell, container, or root config) is the top-level application responsible for deciding, based on the current URL, which independently-built and independently-deployed microfrontend to fetch and mount into the page. That decision is made by a router living in the shell, and the two most common strategies for encoding "where am I" in the URL are **path-based routing** and **hash-based routing**. ## Path-based routing Path-based routing uses the actual URL path segment — `https://example.com/checkout/cart` — as the routing key. The shell's router (often a thin wrapper around the History API: `pushState`, `replaceState`, `popstate`) inspects `window.location.pathname`, matches it against a route table such as `/checkout/* -> checkout-mfe`, `/account/* -> account-mfe`, and mounts the matching bundle. Because the path is a real URL, every navigation has to resolve to something the web server or CDN can serve: - a `pushState` from an in-app link - a full page load - a bookmarked URL - a server-rendered link from an email That means the server (or edge/CDN rewrite rule) must be configured with a catch-all rule that returns the shell's `index.html` (or an SSR response) for any path that belongs to the app, rather than 404ing, because the actual routing decision happens client-side after the shell's JS boots. This is the same "SPA fallback" problem every single-page app has, just centered in the shell rather than one app. ## Hash-based routing Hash-based routing instead encodes the route after a `#`, e.g. `https://example.com/#/checkout/cart`. The browser treats everything after `#` as a same-document fragment identifier and — critically — **never sends it to the server** as part of the HTTP request. The server only ever sees a request for `/`. This means hash routing needs zero server-side rewrite configuration: any static file host, any CDN, any environment that can serve a single `index.html` works out of the box, because the server is oblivious to what's after the `#`. The shell's router instead listens to the `hashchange` event (or intercepts clicks and manually parses `location.hash`) to decide what to mount. ## The trade-off The trade-off is real and multi-dimensional. Path-based URLs: - are clean, and match user expectations of "real" URLs - are indexable by search engines and social-media link previews - support proper server-side rendering per route (the server can inspect the real path and render the right content before JS loads) Their cost is operational: every environment the app runs in — production CDN, preview deployments, local dev proxy, an embedding host if the shell is iframed — needs the SPA-fallback rewrite rule configured correctly, and misconfiguring it produces hard-to-debug 404s on deep-link refresh or on cold navigation from an external link. Hash-based URLs: - need no such configuration - are trivially embeddable (e.g., a microfrontend embedded inside a third-party CMS page where you don't control server routing at all) - but they are non-SEO-friendly and look unpolished to end users - and make server-side rendering per-route effectively impossible since the server never sees the fragment ## Ownership at the infrastructure layer In a microfrontend context specifically, this choice compounds with ownership: path-based routing lets you cleanly map a URL prefix to a team's deployable unit (`/checkout/*` is unambiguously "the checkout team's territory," enforceable at the CDN/reverse-proxy layer with routing rules that can even route different path prefixes to entirely different origins/servers — true edge-level microfrontend isolation). Hash-based routing collapses everything under one served document, so ownership boundaries can only be enforced in client-side JS, not at the infrastructure layer — any microfrontend's JS could in principle read or rewrite another's hash-route, and there's no server-side way to say "this MFE serves this business path." ## What mature platforms actually do In practice, most mature microfrontend platforms (single-spa's root-config, module-federation-based shells) default to path-based routing because it aligns URLs with business capability boundaries and supports SEO/SSR, accepting the operational cost of getting rewrite rules right in every environment. Hash-based routing survives mainly in legacy migrations, iframe-embedded widgets, or contexts where the team genuinely doesn't control server/CDN configuration.

  • Why does hash-based routing avoid the need for any server-side rewrite configuration?
    The browser never sends the fragment after `#` in the HTTP request line — the server only ever sees a request for the base document (e.g. `/`), so there's nothing for it to 404 on. The shell's JS reads `location.hash` client-side after the single document has already loaded, so no server route table needs to know about individual microfrontend paths.
  • What breaks if a CDN's SPA-fallback rewrite rule for a path-based shell is misconfigured?
    Deep links and hard refreshes on any route other than `/` return a real 404 from the server instead of the shell's index.html, because the server tries to resolve the path as a literal resource before the client-side router ever gets a chance to run. This typically shows up as 'the app works when I click around but breaks on refresh or when someone shares a link.'
  • Can you mix path-based and hash-based routing within the same microfrontend shell?
    Yes, though it's unusual and adds complexity: some platforms use path-based routing for the primary business routes (for SEO/SSR) and hash-based sub-routing inside a single mounted microfrontend for ephemeral UI state (a modal step, a tab) that shouldn't be a server-indexable route of its own.

Path-based routing is like a building with real street addresses each department can put on its own door and have mail delivered directly; hash-based routing is like everyone sharing one lobby address and using a handwritten note pinned inside the door to say which department you actually want.

saying these in an interview costs you the question

  • Says hash routing is purely 'the old way' with no operational trade-off reasoning
  • Doesn't mention the SPA-fallback/catch-all server rewrite requirement for path routing
  • Thinks the server 'sees' and can route on the hash fragment
  • Can't explain why hash routing needs no server config
  • Ignores the SEO/SSR implications entirely

context

open as a page

In a microfrontend shell, how does clicking a link inside one microfrontend (e.g. a 'View Order' link inside a Cart microfrontend that should open a route owned by a separate Orders microfrontend) trigger a client-side navigation instead of a full page reload?

level: middleimportance: must knowfreq 65%

basics

~20 s

The link isn't a normal <a> that reloads the page; the shell's router intercepts the click, updates the browser URL with the History API, notices the new path belongs to a different microfrontend, unmounts the old one, and mounts the new one — all without asking the server for a new page.

open as a page

At an organization with 40+ teams each owning microfrontends registered into a shared shell's route table, what technical and process mechanisms prevent route ownership from silently drifting out of sync with the route table as teams reorganize, rename services, or split ownership of a URL prefix?

level: principalimportance: must knowfreq 35%

basics

~20 s

You need something better than tribal knowledge — a checked-in, machine-readable registry of who owns which URL, automated checks that stop two teams from claiming the same route by accident, and a real process for handing off or splitting ownership when teams reorganize.

open as a page

When a platform team defines a route table like `/checkout/* -> checkout-team`, `/search/* -> search-team`, `/account/* -> account-team` for a microfrontend shell, what problem does this mapping solve, and what happens when two teams need to render content on the same URL segment (e.g. a global site-wide header that appears on every path)?

level: middleimportance: should knowfreq 55%

basics

~20 s

The mapping tells the shell which team's code to load for a given URL, so each team can build and deploy independently. Shared elements like a global header that appear on every page are usually owned by a separate, always-mounted piece rather than by any single route-owning team.

open as a page

A shell app mounts a microfrontend that has its own internal React Router instance for sub-navigation (e.g. tabs within `/account/*`). What specifically needs to be synchronized between the shell's top-level router and the microfrontend's internal router so that browser back/forward and deep-linking both work correctly?

level: seniorimportance: should knowfreq 50%

basics

~20 s

The microfrontend's internal router must not fight the shell's top-level router over control of the browser's URL and back button — usually the MFE's router is scoped to only the part of the path under its own prefix, and both must agree on one shared history stack instead of each keeping a separate idea of 'where we are.'

open as a page

Some microfrontend platforms deliberately use full page reloads (a real HTTP navigation) between top-level sections instead of client-side routing with a shared shell router — e.g. navigating from a marketing site's `/blog/*` to its `/app/*` product experience. When is that the right trade-off instead of building a single client-side-routed shell across all sections?

level: seniorimportance: should knowfreq 40%

basics

~20 s

If two sections are built with totally different tech, teams, or release cycles, and users rarely bounce between them quickly, it's often simpler and safer to just let the browser do a normal page load between them instead of forcing them into one shared client-side router.

open as a page