Why Piral?
Module Federation, Native Federation, single-spa, raw import maps, fully custom solutions — there are many ways to implement micro frontends. This page explains what Piral provides that you'd otherwise spend weeks building, and where other approaches might fit better.
The gap most approaches leave
Module Federation, Native Federation, and single-spa give you the mechanics of loading code across bundle boundaries. What they don't solve out of the box:
- How do you discover which modules to load at runtime?
- How do pilet components register themselves into the host application?
- How do modules contribute UI into each other's pages without coupling?
- How do developers work on a module without running the entire platform?
- What's the CLI workflow for scaffolding, building, and publishing?
These are all problems you'd assemble yourself. Piral has documented, tested opinions on all of them.
Dynamic feed: the runtime composition layer
The app shell calls a feed endpoint on startup and gets back a JSON list of pilets to load — which modules, which versions, from which CDN URLs. This is dynamic at runtime. It means:
- Change what a user sees without redeploying the app shell
- Roll out a pilet to 10% of users for a canary test
- Disable a misbehaving pilet for a specific tenant
- Restore the previous version in seconds — no CI pipeline, no
git revert
Compare this to the static remote manifests of Module Federation or Native Federation: those support versioned deploys but don't natively support per-user targeting, gradual rollouts, or instant rollback. (Piral can still use either as its transport — see Module & Native Federation.)
The emulator: the most underrated DX feature
Here's the common scenario: you're developing a checkout pilet. To test it realistically, you need it running inside the actual app shell — with real layout, real navigation, real shared dependencies — alongside other pilets.
The naive solution is running the full platform locally. That often means spinning up the app shell, a feed service, and multiple other pilets. Infeasible for most developers, especially contractors or new joiners.
Piral's answer: when the app shell builds, it produces an npm package containing a debug build of the shell itself. A pilet developer installs this package and runs pilet debug. They see their pilet running inside the real shell, with hot reload, without any platform access. The emulator is a first-class build artifact.
Distributed importmap sharing — not just the app shell
This is frequently misunderstood. Shared dependencies in Piral are not limited to what the app shell declares.
The app shell's importmap establishes centrally shared packages — React, React Router, your design system. Pilets treat these as externals and never bundle them.
But any pilet can also declare shared packages in its own importmap:
When this pilet loads, it places those packages into side-bundles. If another pilet needs the same package and it's already been resolved by any other pilet, it reuses the existing instance. No duplicate downloads, no central coordination. Teams independently share what they independently need.
The Pilet API: a versioned contract
Every pilet receives a typed API object from the shell. That object is the formal, versioned interface between the app shell and the pilet world. It contains everything a pilet can legitimately do: register pages, register menu items, emit events, show notifications, open modals.
This formalization matters because:
- It versions cleanly — bump the shell version when the API changes
- It's TypeScript-typed — autocomplete shows only what's actually available
- It's extensible — app shell authors add capabilities via plugins, which appear as first-class typed API
Extension slots: first-class UI composition
One of Piral's genuine differentiators. A pilet can declare a named slot; other pilets fill it with components. Neither team imports or knows about the other. The shell's extension registry connects them at runtime.
params.Crucially, this is declarative UI composition, not messaging. Every slot resolves through the engine — so the consumer and provider stay fully decoupled, yet the result is a normal component. You could try to wire cross-pilet UI with events instead, but events map poorly onto the component lifecycle: there's no natural mount/unmount, props don't flow in cleanly, and a prop or state change won't re-render the right thing on its own. An extension slot mounts, renders with params, re-renders when they change, and unmounts — exactly what UIs need. Events stay great for notifications; extension slots are the right tool for rendering.
Module Federation and single-spa don't provide this out of the box. You'd implement it yourself, with all the version coordination that implies.
Where other approaches win
- You're extending an existing setup where it's already configured (Webpack, Rspack, or Next.js — Module Federation 2.0 is no longer Webpack-only)
- You need first-class SSR today (its SSR story is currently more mature — though Piral's lands in v2; see Server-side rendering)
- Your team prefers lower-level control over the loading mechanism
- You want a standards-based transport (native ES modules + import maps) with no bundler lock-in and a minimal runtime
- You're in the Angular ecosystem where it's the common choice
- You only need to wire together a small, fixed set of apps without runtime discovery or targeting
- You want framework-agnostic coordination with no React in the host layer
- You're integrating legacy applications that can't be reasonably wrapped as pilets
These aren't strictly either/or: Piral can sit on top of Module Federation or Native Federation, using them as the transport while adding discovery, targeting, the emulator, and extension slots. For most greenfield micro frontend projects, that combination ships a working system faster than assembling the pieces by hand.