DEV Community

Nazar Usik
Nazar Usik

Posted on

Self-Mounting Micro-Frontends: A Simple Pattern

How to embed modern React/Vue/Svelte into legacy applications without complex orchestration

The Problem

How do you put a micro-frontend project onto a monolithic page?

Not just theoretically, practically. You have a legacy application (JSP, PHP, Django templates, whatever). You want to add modern React components without rewriting everything. You need them to coexist on the same page.

The established solutions are complex: Single-SPA requires runtime orchestration, Module Federation needs Webpack configuration, iFrames create styling headaches. What if there was something simpler?

The Simple Solution

The pattern requires just two things:

1. Custom HTML tags + script tag in your legacy page:


<embed-counter></embed-counter>
<script src="${EMBED_SERVER_URL}/embeds/counter-embed.js"></script>
Enter fullscreen mode Exit fullscreen mode

2. A JavaScript bundle that finds those tags and renders into them

That's it. No complex orchestration. No shared dependencies. No runtime coordination.

Self-Mounting Micro-Frontends Overview

How It Works

The pattern breaks down into three technical challenges:

1. How to pack an application into one file?

Use a bundler with IIFE format. Vite, Webpack, Rollup: all can bundle your entire application into a single JavaScript file that executes immediately when loaded.

// vite.config.js
export default defineConfig({
    plugins: [react()],
    build: {
        rollupOptions: {
            output: {
                entryFileNames: 'counter-embed.js',
                format: 'iife',  // Immediately Invoked Function Expression
                inlineDynamicImports: true
            }
        }
    }
})
Enter fullscreen mode Exit fullscreen mode

IIFE format wraps everything in a function scope, preventing variable collisions between embeds or with the host page.

2. How does the application render itself?

Standard framework mounting, but with custom tag selector. React, Vue, Angular all have mounting APIs. Instead of mounting to #root, mount to all instances of your custom tag:

// React example - main.jsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'

const mountEmbed = () => {
    const containers = document.querySelectorAll('embed-counter')

    containers.forEach(container => {
        const root = ReactDOM.createRoot(container)
        root.render(<App/>)
    })
}

if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', mountEmbed)
} else {
    mountEmbed()
}
Enter fullscreen mode Exit fullscreen mode

The same pattern works for Vue (createApp().mount()), Svelte (new App({ target })), or any framework with a mounting API.

3. How to deliver and orchestrate multiple applications?

A static file server + build orchestration. The simplest version is just serving files, but for multiple embeds you need to coordinate builds.

Embed Server Implementation

Two approaches work well depending on your stack:

Option A: Java + Spring Boot + Gradle

Spring Boot serves static files by default. Gradle can orchestrate frontend builds:

Embed Build Flow

The key insight: it doesn't have to be Gradle. The same pattern works with npm workspaces, Turborepo, or any build tool. The point is coordinating multiple frontend builds and collecting outputs in one place.

Build configuration:

// Root build.gradle
tasks.register('deploy') {
    dependsOn ':embeds:counter-embed:installEmbed'
    dependsOn ':embeds:todo-embed:installEmbed'
    dependsOn ':embeds:dashboard-embed:installEmbed'
}

// Each embed's build.gradle
tasks.register('buildEmbed', Exec) {
    workingDir projectDir
    commandLine 'npm', 'install'

    doLast {
        exec {
            workingDir projectDir
            commandLine 'npm', 'run', 'build'
        }
    }
}

tasks.register('installEmbed', Copy) {
    dependsOn buildEmbed
    from 'dist'
    into "${rootProject.projectDir}/src/main/resources/static/embeds"
}
Enter fullscreen mode Exit fullscreen mode

One Gradle command builds all embeds and the Spring Boot server. Embeds end up in static resources, served by Spring Boot.

Usage in legacy page:

<%-- Legacy JSP page --%>
<html>
<body>
    <h1>Legacy Application</h1>

    <%-- Existing legacy content --%>
    <jsp:include page="legacy-component.jsp"/>

    <%-- Modern embed mount point --%>
    <embed-counter></embed-counter>
    <script src="${EMBED_SERVER_URL}/embeds/counter-embed.js"></script>

    <%-- More legacy content --%>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

The EMBED_SERVER_URL is an environment variable. Different environments (dev, staging, prod) point to different embed server instances.

Option B: Node.js + Express + npm workspaces

Lighter alternative using JavaScript tooling throughout:

// Express server
import express from 'express'
import path from 'path'

const app = express()

// Serve embeds
app.use('/embeds', express.static(path.join(__dirname, '../public/embeds')))

// CORS for cross-origin embedding
app.use((req, res, next) => {
    res.header('Access-Control-Allow-Origin', '*')
    res.header('Access-Control-Allow-Methods', 'GET')
    next()
})

app.listen(8080, () => {
    console.log('Embed server running on port 8080')
})
Enter fullscreen mode Exit fullscreen mode

npm workspaces coordinate multiple embed builds:

{
  "name": "embed-server-nodejs",
  "workspaces": [
    "packages/server",
    "embeds/*"
  ],
  "scripts": {
    "build": "npm run build:embeds",
    "build:embeds": "npm run build --workspaces --if-present",
    "start": "npm start --workspace=packages/server"
  }
}
Enter fullscreen mode Exit fullscreen mode

Same pattern, different tooling. Choose based on your existing stack.

Both implementations are available:

Why This Pattern Matters

The embed server itself is simple: static file serving + build orchestration.

But it's a foundation that enables other patterns:

Runtime configuration: Embeds can fetch config from URLs and apply it at runtime:


<embed-entity config-url="${CONFIG_URL}/entity-form-config.json"></embed-entity>
Enter fullscreen mode Exit fullscreen mode

The embed fetches config, merges with defaults, applies theming. Without this pattern, config changes require rebuilding. (See multi-layer config article)

Complex form systems: Self-contained forms with dependencies become simple:


<embed-entity entity-id="${ENTITY_ID}" mode="edit" config-url="${CONFIG_URL}"></embed-entity>
Enter fullscreen mode Exit fullscreen mode

One tag, entire complex form system loaded. Stores, subscriptions, field dependencies, data loading: all encapsulated. (See declarative forms article)

Consistent component libraries: Share styling across embeds while keeping them independent. (See component wrapper article)

Everything builds on custom HTML tags that self-mount applications.

Production Considerations

CORS configuration: Cross-domain embedding requires careful CORS setup. The monolith and embed server run on different domains, so API calls need proper CORS headers for each environment (dev, staging, prod) with allowlist entries.

Authentication: Session-based auth from the host page needs to pass to embeds. Common approach:

  1. Reading the auth cookie from the host page
  2. Including it in embed API calls
  3. Configuring this in the multi-layer config
{
  "fetch": {
    "authorization": {
      "source": {
        "type": "cookie",
        "attributeName": "JSESSIONID"
      },
      "destination": {
        "type": "header",
        "attributeName": "Authorization"
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

CSS isolation: No Shadow DOM, no automatic isolation. Options:

  • CSS Modules in Vite
  • Strict naming conventions (embed-name__component-name__element)
  • Component wrapper library providing consistent base styles

Bundle sizes: Each embed includes its own copy of React. Simple counter ~144 KB, complex forms 500+ KB. Mitigations:

  • Code splitting where possible
  • Lazy-load heavy dependencies
  • Accept the trade-off (simplicity over optimization)

Trade-offs

Build complexity: Adding a new embed requires multiple steps:

  1. Create React project
  2. Configure Vite for IIFE output
  3. Add build orchestration (Gradle/npm)
  4. Register in root build system
  5. Configure custom HTML tag name
  6. Test in host page

A CLI tool (similar to create-react-app or Angular CLI) could automate much of this: generating project scaffolding, Vite config, build files, and registering the embed. Or at minimum, starter templates would reduce the manual setup significantly.

Performance: Each embed bootstraps independently. Multiple embeds on one page = multiple React instances, duplicate libraries. Not optimal.

Debugging challenges: When something breaks, the issue could be:

  • Embed code
  • Build configuration
  • Build orchestration copying files
  • CORS
  • Host page environment

Multiple layers make troubleshooting harder.

Dependency management: No shared dependencies between embeds. React updates require updating every embed, rebuilding everything, redeploying everything.

What It Enables

Independent deployment: Update one embed, deploy just that embed. The monolith doesn't care. Other embeds don't care.

Team scalability: Different teams own different embeds, work in parallel without coordination. No cross-team dependencies.

Technology freedom: Use Svelte for one embed, React for another, Vue for a third. The host page doesn't care, it's just a script tag.

Gradual migration: No complete rewrite needed. Carve out pieces, modernize as embeds, leave the rest. Zero risk incremental approach.

Legacy coexistence: Old JSP pages and new React embeds on the same page. Legacy toolbar with modern form. Everything works together.

When to Use This Pattern

This pattern works best when:

  • Modernizing legacy applications incrementally without full rewrites
  • Team independence is more valuable than shared dependencies
  • Technology flexibility matters (different frameworks for different embeds)
  • Deployment independence is critical
  • Simplicity is preferred over optimization

It's less suitable when:

  • All embeds share heavy dependencies (Module Federation better)
  • Performance is critical (duplicate libraries = overhead)
  • Single team owns everything (simpler patterns exist)
  • Host application can run orchestration logic (Single-SPA better)

The Technical Details (For Those Who Care)

If you want to see the actual implementation:

Java + Spring Boot + Gradle:

Node.js + Express + npm workspaces:

The Core Pattern:

  1. Build: Each embed bundles to single IIFE JavaScript file
  2. Deploy: Embed server serves these files as static resources
  3. Embed: Custom HTML tag + script tag in host page
  4. Mount: Embed finds its tags and mounts React/Vue/Svelte

That's it. No fancy runtime orchestration. No complex dependency sharing. No module federation. Just HTML tags that know how to fill themselves with content.

Simple. Maybe too simple for "proper" micro-frontends, but it solves the legacy modernization problem effectively.

Comparison with Other Approaches

vs. Single-SPA:

  • Self-mounting: Simpler, no orchestration needed
  • Single-SPA: Better shared dependencies, more features, steeper learning curve

vs. Module Federation:

  • Self-mounting: Works with any host, including legacy pages that can't run orchestration
  • Module Federation: Better performance, shared dependencies, requires Webpack knowledge

vs. iFrames:

  • Self-mounting: Easier styling coordination, lighter weight
  • iFrames: Better isolation, but heavier and harder to style consistently

vs. Full rewrite:

  • Self-mounting: Incremental, zero risk, works alongside legacy
  • Full rewrite: Clean slate, but high risk, can't deliver features during rewrite

Wrapping Up

The self-mounting embed pattern bridges legacy and modern:

  • Custom HTML tags provide simple integration points
  • IIFE bundles prevent collisions
  • Static file serving handles delivery
  • Build orchestration coordinates multiple embeds

It enables gradual modernization without stopping feature delivery. The simplicity trades performance for independence and flexibility.

This foundation makes other patterns possible: runtime configuration, complex form systems, shared component libraries; all built on the same principle: HTML tags that know how to render themselves.


Author: Nazar Usik

GitHub: embed-server | embed-server-nodejs

Related: This enables the multi-layer config, declarative forms,
and component wrappers described in other articles.

Top comments (0)