Type something to search...
Next.js Building micro-frontends with multi-zones

Next.js Building micro-frontends with multi-zones

There's a particular kind of pain that shows up in large Next.js applications well before they become unmaintainable: build times creep up, the dependency graph balloons because one team's charting library ends up in every bundle, and a change to the marketing site's footer somehow requires redeploying the entire dashboard. Multi-Zones is Next.js's answer to that specific pain — a way to split one large application on one domain into several genuinely separate Next.js applications, each owning a slice of the URL space, deployed and built independently, while looking to the end user like a single seamless site.

This is Next.js's take on micro-frontends, and it's worth understanding both what it buys you and what it costs, because the trade-offs are real in both directions.

The core idea

Say your app has three natural regions: /blog/* for all blog content, /dashboard/* for logged-in users, and everything else on /*. Under Multi-Zones, these become three separate Next.js applications, each independently built and deployed, each served under the same domain so a visitor never sees a seam. The blog team can ship a typo fix without touching the dashboard's build pipeline. The dashboard team can pull in a heavy charting dependency without it costing the marketing pages a single kilobyte of bundle size. Each "zone" can even use a completely different framework if a team genuinely needs to — Multi-Zones doesn't require every zone to be Next.js, only the pieces that are.

Soft navigation vs. hard navigation — the trade-off that matters most

This is the detail that determines whether Multi-Zones is a good fit for your specific app, so it's worth understanding precisely before adopting the pattern.

Within a zone, navigating between pages is a soft navigation — the same client-side transition you get anywhere else in a Next.js app, no full page reload, state and connections preserved.

Between zones, navigating is a hard navigation — a full browser navigation that unloads the current page's JavaScript entirely and loads the new zone's bundle from scratch, exactly like clicking a link to an entirely different website (because, architecturally, that's essentially what's happening).

The practical consequence: if / and /dashboard live in different zones, a user clicking from your homepage into their dashboard pays the cost of a full page reload — losing any client-side state, refetching everything, repainting from a blank page — every single time. If those two routes are visited together frequently, that's a real, felt performance regression for your users. The rule of thumb the docs give is straightforward: pages that are visited together frequently should live in the same zone. Split along boundaries your users rarely cross in a single session, not along boundaries that are merely convenient for your org chart.

Defining a zone

A zone is just an ordinary Next.js application, with one addition: an assetPrefix so its JavaScript and CSS don't collide with another zone's assets on the same domain.

// next.config.js (the blog zone)
/** @type {import('next').NextConfig} */
const nextConfig = {
  assetPrefix: "/blog-static",
};

With this set, the zone's Next.js assets get served under /blog-static/_next/... instead of the default /_next/..., which is what prevents two zones from stepping on each other's static files when they share a domain. The one application handling everything not claimed by a more specific zone — your default, catch-all app — doesn't need an assetPrefix at all, since it owns whatever's left over by definition.

One historical wrinkle worth flagging in case you're on an older codebase or reading older tutorials: versions of Next.js before 15 also needed an explicit rewrite to route static asset requests correctly alongside the assetPrefix. That's no longer necessary as of Next.js 15 onward — if you're on 16.3 like this project, you can skip that extra rewrite entirely; it's dead weight carried over from older setup guides.

Routing requests to the right zone

Once you have multiple zone applications running, something needs to decide, per request, which one handles it. You have two real options.

Rewrites (the recommended default). One of your Next.js apps — typically the default/catch-all zone — acts as the front door for the whole domain, using rewrites() in next.config.js to forward specific path prefixes to the other zones' actual deployed URLs:

// next.config.js
async rewrites() {
  return [
    {
      source: '/blog',
      destination: `${process.env.BLOG_DOMAIN}/blog`,
    },
    {
      source: '/blog/:path+',
      destination: `${process.env.BLOG_DOMAIN}/blog/:path+`,
    },
    {
      source: '/blog-static/:path+',
      destination: `${process.env.BLOG_DOMAIN}/blog-static/:path+`,
    },
  ]
}

Notice there are three rewrite rules for what looks like one zone: the bare /blog path, the /blog/:path+ catch-all beneath it, and — easy to forget — a separate rule for /blog-static/:path+ to route that zone's actual JS/CSS assets through as well. Missing that third rule is a common failure mode: the page HTML loads fine (because the page-level rewrite works), but the page renders unstyled and non-interactive, because its scripts and stylesheets 404 through the front-door app that never learned to forward them.

destination needs to be a full URL — scheme and domain included — pointing at wherever that zone is actually deployed. Handy detail for local development: there's nothing stopping this from pointing at localhost with a different port while you're developing, letting you run all your zones side-by-side on one machine and still exercise the full rewrite-based routing.

The one hard constraint here: URL paths must be unique to exactly one zone. Two zones both trying to claim /blog is a routing conflict Next.js has no way to resolve for you — you have to design your path space so it partitions cleanly, up front.

Proxy, for routing decisions that need to be dynamic. Rewrites are the lower-latency choice because they're resolved as static configuration — no code runs to make the routing decision. But if you need the routing itself to depend on runtime logic (a feature flag deciding whether a given path should go to the new zone or the old one during a gradual migration, for instance), that's what proxy.js is for:

// proxy.js
export async function proxy(request) {
  const { pathname, search } = request.nextUrl;
  if (pathname === "/your-path" && myFeatureFlag.isEnabled()) {
    return NextResponse.rewrite(`${rewriteDomain}${pathname}${search}`);
  }
}

Reach for this only when the routing decision genuinely can't be made statically — it's slower than a plain rewrite because it runs code on every matching request, and it's easy to reach for out of habit when a plain rewrites() rule would have done the job with less overhead.

Linking between zones: use a plain <a> tag, not <Link>

This is the single most common mistake teams make adopting Multi-Zones, and it's worth memorizing rather than just reading once: any link that crosses a zone boundary must be a plain HTML <a> tag, not Next.js's <Link> component.

The reason is mechanical, not a style preference. <Link> is built to prefetch and soft-navigate to any relative path it's given — that's its entire purpose within a single app. Pointed at a path that actually lives in a different zone (a different deployed application entirely), that machinery has nothing valid to prefetch or soft-navigate to — the destination isn't part of the current zone's route manifest at all. Using <Link> across a zone boundary doesn't necessarily throw a loud error; depending on your setup it can silently misbehave, which makes it a genuinely sneaky bug to chase if you don't already know the rule. Within a zone, keep using <Link> as normal — this restriction is specifically about crossing between zones.

Sharing code across zones

Since each zone is its own deployable application, they can live in entirely separate repositories if that suits your org — but in practice, most teams find a monorepo considerably easier for sharing UI components, types, and utilities across zones without the overhead of publishing and versioning internal packages for every shared change. If your zones genuinely do live in separate repos, public or private NPM packages are the fallback for sharing code, at the cost of a slower iteration loop (publish, bump, install, repeat) compared to a monorepo's instant cross-package visibility.

One coordination problem that's easy to underestimate: zones deploy independently, which means they can easily end up on different release schedules. If a new feature spans multiple zones (a new nav item that needs to render consistently everywhere, say), feature flags become the practical way to enable it in unison once every zone's deploy has actually landed, rather than hoping all your independent deploy pipelines happen to finish at the same moment.

Server Actions need explicit origin allowlisting

If any of your zones use Server Actions, there's a configuration detail specific to Multi-Zones that's easy to miss: your user-facing domain is now serving multiple applications, and Next.js's Server Action origin check needs to know that's expected and safe.

// next.config.js
const nextConfig = {
  experimental: {
    serverActions: {
      allowedOrigins: ["your-production-domain.com"],
    },
  },
};

Skip this and Server Actions in a zone can fail their origin validation specifically because of the multi-zone setup — a failure mode that looks identical to a generic CSRF-style rejection and gives no hint that "you're running Multi-Zones and forgot this config option" is the actual cause.

When this pattern is (and isn't) worth it

Multi-Zones earns its complexity when you have genuinely independent teams who need independent build and deploy cadences, when one part of the app has a bundle-size problem that's actively hurting an unrelated part, or when different sections of the product have fundamentally different technical requirements (one section needs heavy client-side interactivity, another is purely static marketing content that a different team owns entirely).

It's the wrong tool if your "problem" is really just "our app feels big" without a specific, identifiable pain — the hard-navigation cost between zones is real and permanent, and paying it for organizational tidiness rather than an actual scaling bottleneck usually isn't worth the user-facing regression. Start as one application, and reach for Multi-Zones when a specific, named problem — build time, bundle bloat, deploy coupling between unrelated teams — actually shows up.

Key Takeaways

ConcernHow Multi-Zones handles it
Navigation within a zoneSoft navigation (normal client-side transition)
Navigation between zonesHard navigation (full page reload) — plan your zone boundaries around this
Asset collisionsSolved via assetPrefix, one per zone (not needed on the default/catch-all zone)
Routing requests to zonesrewrites() by default; proxy.js only for genuinely dynamic routing decisions
Linking across zonesPlain <a> tag — never <Link>
Linking within a zone<Link> as usual
Code sharingMonorepo preferred; NPM packages if zones live in separate repos
Server ActionsMust configure allowedOrigins for the shared production domain

Multi-Zones lets several independently-deployed Next.js applications present themselves as one seamless site, and the mechanics — assetPrefix, rewrites, the <a> vs. <Link> distinction — are genuinely simple once you've seen them once. The judgment call that actually matters is upstream of all of that: deciding where your zone boundaries go, based on which pages your users actually move between together, not based on which team happens to own which part of the codebase.

Tags :
Share :

Related Posts

Can Next.js Be Used with GraphQL?

Can Next.js Be Used with GraphQL?

Next.js and GraphQL are two powerful technologies that have gained significant traction in the web development community. Next.js, a React-based fram

Dive Deeper
How does Next.js differ from Create React App?

How does Next.js differ from Create React App?

In the world of modern web development, React.js has emerged as a dominant force due to its flexibility, performance, and extensive ecosystem. Two po

Dive Deeper
How does Next.js handle image optimization?

How does Next.js handle image optimization?

In modern web development, image optimization plays a critical role in enhancing user experience and improving site performance. Large, unoptimized i

Dive Deeper