Server-Side Micro-Frontend Composition: One Page, Many Teams
Server-side micro-frontend composition lets independent teams own fragments of one page. The hard part is the ownership boundary and the versioned consistency contract, not the rendering tech.
A single high-traffic page, such as a checkout or storefront, often needs contributions from several teams who must ship independently. A frontend monolith forces them onto one release train, so one team’s slow build or broken test blocks every other team’s deploy. The mechanism that assembles the page is the easy part; the genuinely hard problem is the ownership boundary plus a versioned consistency contract that keeps many teams visually and behaviourally aligned without a coordinated release. This post argues that for a multi-team, SEO-sensitive page where first paint matters, server-side composition is the default, and shows where client-side composition and iframes win instead.
This is a server-side-first companion to the existing client-side series. If you need the Module Federation mechanics, event-bus wiring, and runtime debugging, start with Micro Frontend Architecture Fundamentals and Implementation Patterns. Here the focus is the server-side assembly model and the contract that holds it together.
The fragment as a small full-stack app
The unit of ownership in server-side composition is the fragment: a self-contained app with its own server-rendered HTML, its own client bundle that hydrates, and its own state. A team owns the fragment end to end. The composition layer never reaches inside it; it only fetches the fragment’s rendered markup by URL.
The micro-frontends.org community guide makes this concrete. Each team runs its own server, and the rendered output is reachable as a plain URL: a request to /blue-buy?sku=t_porsche returns a fully-rendered <button>, ready to drop into a page. Podium uses the same shape and names it a “podlet”: a podlet server “is responsible for generating HTML fragments which can then be used in a [layout] server to compose a full HTML page.” The pattern is framework-agnostic, with Express as a first-class integration.
A minimal fragment server in Node looks like this. It server-renders its own React subtree and exposes the markup at a URL.
// mini-cart fragment: its own server, its own React tree
import express from "express";
import { renderToString } from "react-dom/server";
import { MiniCart } from "./MiniCart";
const app = express();
app.get("/fragment/mini-cart", async (req, res) => {
const cart = await loadCart(req.query.userId as string);
const html = renderToString(<MiniCart cart={cart} />);
// Tell the composition layer where this fragment's assets live
res.setHeader("Link", '</assets/mini-cart.js>; rel="fragment-script"');
res.send(html);
});
app.listen(3002);
The composition layer fetches that URL. The fragment owns its render, its data fetch, and its assets. Nothing about the cart’s internals leaks into the host.
The composition layer
At request time, a layout host fetches each fragment’s markup and stitches the responses into one document streamed to the browser. The canonical low-tech mechanism is Server Side Includes, where the host resolves placeholders like <!--#include virtual="/blue-buy?sku=t_porsche" --> before sending the page. Cam Jackson’s Martin Fowler article shows the same idea with an Nginx ssi on; directive, and frames server-side template composition as the “un-novel” but legitimate baseline rather than a lesser option.
A template host gives more control than raw SSI. Tailor, an archived but well-documented OSS project (read-only since 2022), is clear prior art for this shape: fragments are declared as custom elements and carry attributes the host honours at composition time. Podium is an actively maintained alternative built on the same idea.
<!-- Layout template resolved by the composition host -->
<html>
<head><title>Checkout</title></head>
<body>
<fragment src="https://header.internal/fragment"></fragment>
<fragment
src="https://cart.internal/fragment/mini-cart"
primary
timeout="2000"
fallback-src="https://cart.internal/fragment/mini-cart-empty">
</fragment>
<fragment
src="https://reco.internal/fragment/recommended"
async>
</fragment>
</body>
</html>
The attributes carry the operational contract. In Tailor, timeout defaults to 3000 ms, primary lets one fragment set the page response code, async defers a fragment to the end of the body, and fallback-src names a URL to serve when the fragment times out or errors. That last attribute matters most: it is the per-fragment circuit breaker that stops one slow team from stalling the whole page.
The request-time flow is a fan-out and a stitch.
Note: A streaming host can start sending the document head and early fragments before slow ones resolve, so the slowest fragment delays only its own region rather than the whole first byte. Marking expensive fragments
asyncand deferring them past first paint is the main lever here.
Cross-cutting concerns each fragment owns
Internationalisation, auth tokens, observability, and money formatting are owned by each fragment, not injected by the host. To stop fragments colliding in a shared DOM, micro-frontends.org’s “Establish Team Prefixes” rule applies: namespace CSS classes, custom events, local storage keys, and cookies per team. A cart- prefix on classes and a cart: prefix on events make ownership legible and prevent one team’s global selector from restyling another team’s markup.
This is also why the host stays thin. The more the host knows about a fragment’s internals, the more coupling you reintroduce. The host’s job is to fetch, stitch, time out, and fall back; everything else belongs to the team that owns the fragment.
The consistency contract
This is the part that actually decides whether the architecture survives contact with real teams. Independent rendering is solved by the fragment model above. What is not solved by it is visual and behavioural consistency: every team’s buttons, spacing, focus rings, and currency formatting will drift apart unless something holds them together. That something is the consistency contract, and it has two halves.
The first half is a versioned shared design system. Consistency comes not from a shared codebase but from a registry of versioned shared packages: a UI component library plus a design-tokens package, each consumed via semver so teams upgrade on their own cadence. Cam Jackson endorses this directly. Shared component libraries give “reduced effort through re-use of code, and visual consistency,” and act as a “living styleguide.” He also warns against building a Foundation Platform too early; better to “let teams create their own components … even if that causes some duplication,” then harvest the proven ones into the shared library later. The best first candidates to share are dumb visual primitives: icons, labels, buttons.
The second half is the composition protocol itself: the fragment URL shape, the attribute conventions (timeout, primary, async, fallback-src), and the namespacing rules. This is the interface between the host and every fragment, and it is what lets a new fragment join the page without the host changing.
There is a real tension to name here. micro-frontends.org says to “Isolate Team Code: don’t share a runtime, even if all teams use the same framework, and don’t rely on shared state or global variables.” A shared UI library is, by definition, shared code. Both positions are correct at different layers. The resolution is precise: share build-time packages consumed via semver, but never share a runtime singleton or global state across fragments. Tokens and components ship as versioned npm packages each team pins; they do not become a live shared object in the browser.
The cost of getting this wrong is exactly what Cam Jackson warns about. Externalising common dependencies “re-introduces some build-time coupling … an implicit contract … we all must use these exact versions … If there is a breaking change … a big coordinated upgrade effort and a one-off lockstep release event. This is everything we were trying to avoid …” So the engineering discipline that makes server-side composition work is not the SSI stitching. It is contract management: semver discipline, additive-only token changes, and deprecation windows long enough that teams upgrade on their own schedule rather than in lockstep.
Server vs client vs iframe
The composition style is a decision, and it follows from the page, not from fashion. Server-side composition is the default for a multi-team page where first paint and SEO matter. Reach for client-side runtime composition when the page is an authenticated app shell where SEO is moot and in-browser route transitions matter more than first byte. Reserve iframes for hard isolation, where the content is untrusted third-party code or legacy you cannot trust to share a DOM.
| Factor | Server-side composition | Client-side (Module Federation / single-spa) | Iframe |
|---|---|---|---|
| SEO / first paint | Strong (HTML arrives rendered) | Weak unless paired with SSR | Weak |
| In-browser route transitions | Full reload by default | Strong (SPA shell) | Weak |
| Style / runtime isolation | Manual (prefixes, scoped CSS) | Manual (shared singletons risk skew) | Strong (native) |
| Team coupling | Low (URL + attribute contract) | Build / runtime version contract | Lowest |
| Failure blast radius | Per-fragment timeout + fallback-src | Shared-dep break can ripple | Contained |
| Deep page integration | Good | Best | Poor (history, responsive layout) |
The client-side override is well served by the existing series. Module Federation 2.0 adds a Federation Runtime, a Manifest, dynamic type hints, and a runtime plugin system over Webpack 5’s built-in version, positioned for large-scale in-browser composition. single-spa composes apps in the browser via a root-config plus an import map, namespaces modules with an orgName, and can optionally add SSR through its Layout Engine. Both are the right tool when the page is an app shell behind login.
Trade-offs
Server-side composition pays for its strengths. The most important cost is latency coupling: as micro-frontends.org puts it, “the slowest fragment determines the response time of the whole page.” The mitigations are the ones the contract already provides: cache fragment responses, set a per-fragment timeout with a fallback-src, and mark expensive fragments async so they defer past first paint and load client-side. You also operate more infrastructure, since each fragment is its own server alongside the composition host.
Client-side composition trades that for a different cost. Each micro-frontend can ship its own copy of React, so, in Cam Jackson’s words, “customers download React n times.” Externalising shared dependencies fixes the payload but reintroduces the build-time coupling and lockstep-upgrade risk described above. Iframes give the strongest isolation natively, but routing and history, responsive layout, and deep integration are hard; Cam Jackson notes the reluctance to use them is partly aesthetic and partly justified.
Common pitfalls
Treating micro-frontends as a rendering-tech choice rather than an organisational one is the root mistake. The page is split because teams are split; the org boundary is the design. Team Topologies frames the same idea as stream-aligned teams consuming a platform via an X-as-a-Service interaction mode, and the composition layer is exactly that platform service. If the team boundaries do not line up with the page regions, no rendering technology will save the architecture.
The other recurring failures all map to skipping the contract:
- No shared design system, or building a Foundation Platform too early. The first leads to visual drift; the second leads to churn. Harvest proven primitives into the shared library later rather than mandating one up front.
- Synchronous fan-out with no per-fragment timeout or fallback. One slow team then stalls the whole page. Every fragment needs a
timeoutand afallback-src. - Sharing global state or duplicated framework instances across fragments. This reintroduces the runtime coupling you adopted micro-frontends to avoid. Isolate team code and namespace via team prefixes.
- Treating the shared UI library version as “everyone upgrades together.” That is the lockstep release you were trying to escape. The contract must allow independent upgrade cadence through semver ranges and deprecation windows.
Closing
For a multi-team, SEO-sensitive page where first paint matters, server-side composition is the default: fragments are small full-stack apps, the host fetches and stitches their markup with per-fragment timeouts and fallbacks, and a versioned shared design system plus a stable composition protocol keep teams aligned without a lockstep release. The boundary is clear. When the page is an authenticated app shell where in-browser route transitions matter more than first paint, move to client-side composition with Module Federation or single-spa; when the content is untrusted or untrustworthy legacy, use an iframe for native isolation. Before choosing a rendering mechanism, decide where the team and ownership boundaries fall and how the design-system contract will be versioned. That decision, not the SSI stitching, is the one that determines whether the architecture holds.
References
- Micro Frontends (micro-frontends.org) - Canonical community guide: isolate team code, universal rendering, team prefixes, and the SSI plus custom-element fragment examples.
- Cam Jackson, “Micro Frontends” (martinfowler.com) - Foundational article on server-side template composition, shared component libraries, and the build-time coupling cost of shared dependencies.
- @podium/podlet API - Podlet as a page-fragment server composed by a layout server; framework-agnostic with Express as a first-class integration.
- Tailor (GitHub) - OSS streaming layout service illustrating
<fragment>attributes (timeout,primary,async,fallback-src). Archived/read-only since 2022 (last release 3.9.2, 2018); cited as a clear illustration of the contract, not a maintained dependency. Use Podium for a maintained option. - single-spa Getting Started - Client-side composition via root-config plus import map, with an optional SSR Layout Engine.
- Module Federation - Federation Runtime, Manifest, and runtime plugins for large-scale client-side composition. Fast-moving; verify version framing before relying on specifics.
- Team Topologies — Key Concepts - Stream-aligned versus platform teams and the X-as-a-Service interaction mode that grounds the ownership-boundary argument.
- Micro Frontend Architecture Fundamentals - Companion series covering the four composition types and the team-structure preconditions.
- Micro Frontend Implementation Patterns - Companion series covering Module Federation setup, cross-app communication, and routing for client-side composition.
Related posts
A practical comparison of TypeScript AI SDKs for building agents: Vercel AI SDK, OpenAI Agents SDK, and AWS Bedrock, with code examples and decision frameworks.
How SOLID principles apply to modern JavaScript: practical examples with TypeScript, React hooks, and functional patterns, plus when they're overkill.
Bake a single PII branded type into your observability API signatures so TypeScript rejects sensitive fields at the call site, before any runtime redactor sees them.
What Aurora Serverless v2 is under the hood: the shared storage layer, ACU-driven compute, the Caspian substrate, scale-to-zero, and mixed-mode clusters.
HMAC-SHA-256 for webhooks, signed URLs, and internal auth, with runnable code in three languages and the boundary where digital signatures take over.