Updated at 6:55 a.m. PT
Today, we’re introducing a new Worker template for Vertical Microfrontends (VMFE). This template allows you to map multiple independent Cloudflare Workers to a single domain, enabling teams to work in complete silos — shipping marketing, docs, and dashboards independently — while presenting a single, seamless application to the user.
Most microfrontend architectures are "horizontal", meaning different parts of a single page are fetched from different services. Vertical microfrontends take a different approach by splitting the application by URL path. In this model, a team owning the `/blog` path doesn't just own a component; they own the entire vertical stack for that route – framework, library choice, CI/CD and more. Owning the entire stack of a path, or set of paths, allows teams to have true ownership of their work and ship with confidence.
Teams face problems as they grow, where different frameworks serve varying use cases. A marketing website could be better utilized with Astro, for example, while a dashboard might be better with React. Or say you have a monolithic code base where many teams ship as a collective. An update to add new features from several teams can get frustratingly rolled back because a single team introduced a regression. How do we solve the problem of obscuring the technical implementation details away from the user and letting teams ship a cohesive user experience with full autonomy and control of their domains?
Vertical microfrontends can be the answer. Let’s dive in and explore how they solve developer pain points together.
What are vertical microfrontends?
A vertical microfrontend is an architectural pattern where a single independent team owns an entire slice of the application’s functionality, from the user interface all the way down to the CI/CD pipeline. These slices are defined by paths on a domain where you can associate individual Workers with specific paths:
/ = Marketing
/docs = Documentation
/blog = Blog
/dash = Dashboard
We could take it a step further and focus on more granular sub-path Worker associations, too, such as a dashboard. Within a dashboard, you likely segment out various features or products by adding depth to your URL path (e.g. /dash/product-a) and navigating between two products could mean two entirely different code bases.
Now with vertical microfrontends, we could also have the following:
/dash/product-a = WorkerA
/dash/product-b = WorkerB
Each of the above paths are their own frontend project with zero shared code between them. The product-a and product-b routes map to separately deployed frontend applications that have their own frameworks, libraries, CI/CD pipelines defined and owned by their own teams. FINALLY.
You can now own your own code from end to end. But now we need to find a way to stitch these separate projects together, and even more so, make them feel as if they are a unified experience.
We experience this pain point ourselves here at Cloudflare, as the dashboard has many individual teams owning their own products. Teams must contend with the fact that changes made outside their control impact how users experience their product.
Internally, we are now using a similar strategy for our own dashboard. When users navigate from the core dashboard into our ZeroTrust product, in reality they are two entirely separate projects and the user is simply being routed to that project by its path /:accountId/one.
Visually unified experiences
Stitching these individual projects together to make them feel like a unified experience isn’t as difficult as you might think: It only takes a few lines of CSS magic. What we absolutely do not want to happen is to leak our implementation details and internal decisions to our users. If we fail to make this user experience feel like one cohesive frontend, then we’ve done a grave injustice to our users.
To accomplish this sleight of hand, let us take a little trip in understanding how view transitions and document preloading come into play.
When we want to seamlessly navigate between two distinct pages while making it feel smooth to the end user, view transitions are quite useful. Defining specific DOM elements on our page to stick around until the next page is visible, and defining how any changes are handled, make for quite the powerful quilt-stitching tool for multi-page applications.
There may be, however, instances where making the various vertical microfrontends feel different is more than acceptable. Perhaps our marketing website, documentation, and dashboard are each uniquely defined, for instance. A user would not expect all three of those to feel cohesive as you navigate between the three parts. But… if you decide to introduce vertical slices to an individual experience such as the dashboard (e.g. /dash/product-a & /dash/product-b), then users should never know they are two different repositories/workers/projects underneath.
Okay, enough talk — let’s get to work. I mentioned it was low-effort to make two separate projects feel as if they were one to a user, and if you have yet to hear about CSS View Transitions then I’m about to blow your mind.
What if I told you that you could make animated transitions between different views — single-page app (SPA) or multi-page app (MPA) — feel as if they were one? Before any view transitions are added, if we navigate between pages owned by two different Workers, the interstitial loading state would be the white blank screen in our browser for some few hundred milliseconds until the full next page began rendering. Pages would not feel cohesive, and it certainly would not feel like a single-page application.
Appears as multiple navigation elements between each site.
If we want elements to stick around, rather than seeing a white blank page, we can achieve that by defining CSS View Transitions. With the code below, we’re telling our current document page that when a view transition event is about to happen, keep the nav DOM element on the screen, and if any delta in appearance exists between our existing page and our destination page, then we’ll animate that with an ease-in-out transition.
All of a sudden, two different Workers feel like one.
@supports (view-transition-name: none) {
::view-transition-old(root),
::view-transition-new(root) {
animation-duration: 0.3s;
animation-timing-function: ease-in-out;
}
nav { view-transition-name: navigation; }
}
Appears as a single navigation element between three distinct sites.
Transitioning between two pages makes it look seamless — and we also want it to feel as instant as a client-side SPA. While currently Firefox and Safari do not support Speculation Rules, Chrome/Edge/Opera do support the more recent newcomer. The speculation rules API is designed to improve performance for future navigations, particularly for document URLs, making multi-page applications feel more like single-page applications.
Breaking it down into code, what we need to define is a script rule in a specific format that tells the supporting browsers how to prefetch the other vertical slices that are connected to our web application — likely linked through some shared navigation.
<script type="speculationrules">
{
"prefetch": [
{
"urls": ["https://product-a.com", "https://product-b.com"],
"requires": ["anonymous-client-ip-when-cross-origin"],
"referrer_policy": "no-referrer"
}
]
}
</script>
With that, our application prefetches our other microfrontends and holds them in our in-memory cache, so if we were to navigate to those pages it would feel nearly instant.
You likely won’t require this for clearly discernible vertical slices (marketing, docs, dashboard) because users would expect a slight load between them. However, it is highly encouraged to use when vertical slices are defined within a specific visible experience (e.g. within dashboard pages).
Between View Transitions and Speculation Rules, we are able to tie together entirely different code repositories to feel as if they were served from a single-page application. Wild if you ask me.
Zero-config request routing
Now we need a mechanism to host multiple applications, and a method to stitch them together as requests stream in. Defining a single Cloudflare Worker as the “Router” allows a single logical point (at the edge) to handle network requests and then forward them to whichever vertical microfrontend is responsible for that URL path. Plus it doesn’t hurt that then we can map a single domain to that router Worker and the rest “just works.”
If you have yet to explore Cloudflare Worker service bindings, then it is worth taking a moment to do so.
Service bindings allow one Worker to call into another, without going through a publicly-accessible URL. A Service binding allows Worker A to call a method on Worker B, or to forward a request from Worker A to Worker. Breaking it down further, the Router Worker can call into each vertical microfrontend Worker that has been defined (e.g. marketing, docs, dashboard), assuming each of them were Cloudflare Workers.
Why is this important? This is precisely the mechanism that “stitches” these vertical slices together. We’ll dig into how the request routing is handling the traffic split in the next section. But to define each of these microfrontends, we’ll need to update our Router Worker’s wrangler definition, so it knows which frontends it’s allowed to call into.
{
"$schema": "./node_modules/wrangler/config-schema.json",
"name": "router",
"main": "./src/router.js",
"services": [
{
"binding": "HOME",
"service": "worker_marketing"
},
{
"binding": "DOCS",
"service": "worker_docs"
},
{
"binding": "DASH",
"service": "worker_dash"
},
]
}
Our above sample definition is defined in our Router Worker, which then tells us that we are permitted to make requests into three separate additional Workers (marketing, docs, and dash). Granting permissions is as simple as that, but let’s tumble into some of the more complex logic with request routing and HTML rewriting network responses.
With knowledge of the various other Workers we are able to call into if needed, now we need some logic in place to know where to direct network requests when. Since the Router Worker is assigned to our custom domain, all incoming requests hit it first at the network edge. It then determines which Worker should handle the request and manages the resulting response.
The first step is to map URL paths to associated Workers. When a certain request URL is received, we need to know where it needs to be forwarded. We do this by defining rules. While we support wildcard routes, dynamic paths, and parameter constraints, we are going to stay focused on the basics — literal path prefixes — as it illustrates the point more clearly.
In this example, we have three microfrontends:
/ = Marketing
/docs = Documentation
/dash = Dashboard
Each of the above paths need to be mapped to an actual Worker (see our wrangler definition for services in the section above). For our Router Worker, we define an additional variable with the following data, so we can know which paths should map to which service bindings. We now know where to route users as requests come in! Define a wrangler variable with the name ROUTES and the following contents:
{
"routes":[
{"binding": "HOME", "path": "/"},
{"binding": "DOCS", "path": "/docs"},
{"binding": "DASH", "path": "/dash"}
]
}
Let’s envision a user visiting our website path /docs/installation. Under the hood, what happens is the request first reaches our Router Worker which is in charge of understanding what URL paths map to which individual Workers. It understands that the /docs path prefix is mapped to our DOCS service binding which referencing our wrangler file points us at our worker_docs project. Our Router Worker, knowing that /docs is defined as a vertical microfrontend route, removes the /docs prefix from the path and forwards the request to our worker_docs Worker to handle the request and then finally returns whatever response we get.
Why does it drop the /docs path, though? This was an implementation detail choice that was made so that when the Worker is accessed via the Router Worker, it can clean up the URL to handle the request as if it were called from outside our Router Worker. Like any Cloudflare Worker, our worker_docs service might have its own individual URL where it can be accessed. We decided we wanted that service URL to continue to work independently. When it’s attached to our new Router Worker, it would automatically handle removing the prefix, so the service could be accessible from its own defined URL or through our Router Worker… either place, doesn’t matter.
Splitting our various frontend services with URL paths (e.g. /docs or /dash) makes it easy for us to forward a request, but when our response contains HTML that doesn’t know it’s being reverse proxied through a path component… well, that causes problems.
Say our documentation website has an image tag in the response <img src="./logo.png" />. If our user was visiting this page at https://website.com/docs/, then loading the logo.png file would likely fail because our /docs path is somewhat artificially defined only by our Router Worker.
Only when our services are accessed through our Router Worker do we need to do some HTML rewriting of absolute paths so our returned browser response references valid assets. In practice what happens is that when a request passes through our Router Worker, we pass the request to the correct Service Binding, and we receive the response from that. Before we pass that back to the client, we have an opportunity to rewrite the DOM — so where we see absolute paths, we go ahead and prepend that with the proxied path. Where previously our HTML was returning our image tag with <img src="./logo.png" /> we now modify it before returning to the client browser to <img src="./docs/logo.png" />.
Let’s return for a moment to the magic of CSS view transitions and document preloading. We could of course manually place that code into our projects and have it work, but this Router Worker will automatically handle that logic for us by also using HTMLRewriter.
In your Router Worker ROUTES variable, if you set smoothTransitions to true at the root level, then the CSS transition views code will be added automatically. Additionally, if you set the preload key within a route to true, then the script code speculation rules for that route will automatically be added as well.
Below is an example of both in action:
{
"smoothTransitions":true,
"routes":[
{"binding": "APP1", "path": "/app1", "preload": true},
{"binding": "APP2", "path": "/app2", "preload": true}
]
}
You can start building with the Vertical Microfrontend template today.
Visit the Cloudflare Dashboard deeplink here or go to “Workers & Pages” and click the “Create application” button to get started. From there, click “Select a template” and then “Create microfrontend” and you can begin configuring your setup.
Check out the documentation to see how to map your existing Workers and enable View Transitions. We can't wait to see what complex, multi-team applications you build on the edge!