Microfrontend Architecture Development for Web Application

Our company is engaged in the development, support and maintenance of sites of any complexity. From simple one-page sites to large-scale cluster systems built on micro services. Experience of developers is confirmed by certificates from vendors.
Development and maintenance of all types of websites:
Informational websites or web applications
Business card websites, landing pages, corporate websites, online catalogs, quizzes, promo websites, blogs, news resources, informational portals, forums, aggregators
E-commerce websites or web applications
Online stores, B2B portals, marketplaces, online exchanges, cashback websites, exchanges, dropshipping platforms, product parsers
Business process management web applications
CRM systems, ERP systems, corporate portals, production management systems, information parsers
Electronic service websites or web applications
Classified ads platforms, online schools, online cinemas, website builders, portals for electronic services, video hosting platforms, thematic portals

These are just some of the technical types of websites we work with, and each of them can have its own specific features and functionality, as well as be customized to meet the specific needs and goals of the client.

Our competencies:
Development stages
Latest works
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1161
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1041
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    822
  • image_crm_chasseurs_493_0.webp
    CRM development for Chasseurs
    847
  • image_website-sbh_0.png
    Website development for SBH Partners
    999
  • image_website-_0.png
    Website development for Red Pear
    451

Web Application Microfrontend Architecture Development

Microfrontends are an organizational pattern, not a technology. Essence: a large frontend application is split into independent parts, each owned by separate teams. Each part is developed, tested, and deployed independently.

This is justified when 4+ teams work on one application, when a monolith has become a deployment bottleneck, or when different parts of the application develop at fundamentally different paces.

What's included in the work

Analysis of the domain and team boundaries, selection of integration approach, shell-architecture design, setup of shared dependencies, design system, communication between microfrontends, CI/CD strategy, documentation.

Integration approaches — comparison

Approach Build-time Run-time Isolation Complexity
NPM packages Yes No No Low
Module Federation No Yes Partial Medium
iframes No Yes Full Low
Web Components No Yes CSS Medium
Single-SPA No Yes Partial High

For most projects, Module Federation (if everything is React/Vue) or Single-SPA (if teams use different frameworks) are optimal.

Step 1 — Analysis of domain boundaries

A microfrontend boundary is a bounded context boundary in the business domain, not technical convenience:

E-commerce platform:
  ├── Catalog Team     → /products, /categories, /search
  ├── Cart Team        → /cart, mini-cart widget
  ├── Checkout Team    → /checkout, /payment
  ├── Account Team     → /profile, /orders, /addresses
  └── Platform Team    → shell, auth, design system, analytics

Poor division: by technical stack (header/sidebar/content), by UI components, by layers (API/state/view).

Step 2 — Shell application

Shell is a thin wrapper without business logic. Responsible only for:

  • top-level routing
  • loading microfrontends
  • shared services (auth, analytics)
  • navigation and layout
// apps/shell/src/App.tsx
import React, { Suspense, lazy } from 'react'
import { Routes, Route, Navigate } from 'react-router-dom'
import { Shell } from './components/Shell'
import { AuthGuard } from './guards/AuthGuard'

// lazy — load only what's needed
const CatalogApp = lazy(() => import('catalog/App'))
const CheckoutApp = lazy(() => import('checkout/App'))
const AccountApp = lazy(() => import('account/App'))

export function App() {
  return (
    <Shell>
      <Routes>
        <Route path="/" element={<Navigate to="/products" replace />} />
        <Route
          path="/products/*"
          element={
            <Suspense fallback={<AppSkeleton name="Catalog" />}>
              <CatalogApp />
            </Suspense>
          }
        />
        <Route
          path="/checkout/*"
          element={
            <AuthGuard>
              <Suspense fallback={<AppSkeleton name="Checkout" />}>
                <CheckoutApp />
              </Suspense>
            </AuthGuard>
          }
        />
        <Route
          path="/account/*"
          element={
            <AuthGuard>
              <Suspense fallback={<AppSkeleton name="Account" />}>
                <AccountApp />
              </Suspense>
            </AuthGuard>
          }
        />
      </Routes>
    </Shell>
  )
}

Step 3 — Shared contracts

Microfrontends communicate through contracts. Contracts need to be versioned and maintain backward compatibility:

// packages/contracts/src/events.ts
// Typed events — published as npm package

export interface CartEvents {
  'cart:item-added': { productId: string; quantity: number; price: number }
  'cart:item-removed': { productId: string }
  'cart:cleared': Record<string, never>
  'cart:checkout-started': { cartTotal: number; itemCount: number }
}

export interface AuthEvents {
  'auth:login': { userId: string; roles: string[] }
  'auth:logout': Record<string, never>
  'auth:token-refreshed': { expiresAt: number }
}

// Typed event bus
type AllEvents = CartEvents & AuthEvents

export class EventBus {
  private emitter = new EventTarget()

  emit<K extends keyof AllEvents>(event: K, detail: AllEvents[K]) {
    this.emitter.dispatchEvent(new CustomEvent(event as string, { detail }))
  }

  on<K extends keyof AllEvents>(event: K, handler: (detail: AllEvents[K]) => void) {
    const listener = (e: Event) => handler((e as CustomEvent).detail)
    this.emitter.addEventListener(event as string, listener)
    return () => this.emitter.removeEventListener(event as string, listener)
  }
}

export const eventBus = new EventBus()

Step 4 — Shared dependencies and design system

The design system is a separate package that all microfrontends receive as an npm dependency:

packages/
  ui/                   — components, tokens, icons
    src/
      components/
      tokens/
      icons/
    package.json
  contracts/            — event and interface types
  shared-config/        — eslint, tsconfig, prettier base configs

In Module Federation, make the UI package singleton: true — all microfrontends use one version:

// in each webpack.config.js / vite.config.ts
shared: {
  '@company/ui': { singleton: true, requiredVersion: '^2.0.0' },
  react: { singleton: true },
  'react-dom': { singleton: true },
}

Step 5 — Routing strategy

Two approaches to routing:

Centralized (shell owns top-level routes):

shell:    /products → loads CatalogApp
catalog:  /products/:id, /products?search=

Delegated (each microfrontend owns its own routes):

shell:    /* → passes to CatalogApp or CheckoutApp
catalog:  handles /products/*, /categories/*

When using React Router, I recommend centralized: shell defines top-level /products/*, inside CatalogApp — its own nested Routes.

Step 6 — Authentication

Auth logic is in shell or in a separate auth-microfrontend. Others only receive the authorization fact:

// packages/auth-client/src/index.ts
export interface AuthContext {
  user: User | null
  token: string | null
  isAuthenticated: boolean
  login: (credentials: Credentials) => Promise<void>
  logout: () => void
  hasPermission: (permission: string) => boolean
}

// AuthProvider in shell
export function AuthProvider({ children }: { children: React.ReactNode }) {
  // token storage, refresh, logout on 401 logic
  const auth = useAuthState()

  return (
    <AuthContext.Provider value={auth}>
      {children}
    </AuthContext.Provider>
  )
}

// In microfrontend — only useAuth()
// import { useAuth } from '@company/auth-client'
// const { user, hasPermission } = useAuth()

Step 7 — Degradation handling

Remote can be unavailable. Shell should degrade gracefully:

function withRemoteFallback<P extends object>(
  remoteLoader: () => Promise<{ default: React.ComponentType<P> }>,
  FallbackComponent: React.ComponentType
) {
  const Remote = lazy(remoteLoader)

  return function RemoteWithFallback(props: P) {
    return (
      <ErrorBoundary
        onError={(error) => {
          monitoring.captureException(error, { tags: { type: 'remote_load_failure' } })
        }}
        fallback={<FallbackComponent />}
      >
        <Suspense fallback={<Spinner />}>
          <Remote {...props} />
        </Suspense>
      </ErrorBoundary>
    )
  }
}

const CatalogApp = withRemoteFallback(
  () => import('catalog/App'),
  () => <ServiceUnavailable name="Catalog" />
)

Step 8 — CI/CD strategy

monorepo (or polyrepo):
  apps/catalog/     → pipeline → CDN catalog.example.com
  apps/checkout/    → pipeline → CDN checkout.example.com
  apps/shell/       → pipeline → CDN example.com

shell stores remoteEntry URL in config received from server:
  /api/mf-config → { catalog: "https://catalog.example.com/...", ... }
# .github/workflows/catalog-deploy.yml
name: Catalog Deploy
on:
  push:
    branches: [main]
    paths: ['apps/catalog/**', 'packages/ui/**']

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: cd apps/catalog && npm ci && npm test && npm run build

  deploy:
    needs: test
    runs-on: ubuntu-latest
    steps:
      - name: Deploy to S3
        run: aws s3 sync apps/catalog/dist s3://$CATALOG_BUCKET --delete

      - name: Notify shell of new version
        run: |
          curl -X POST $SHELL_API/mf-config \
            -H "Authorization: Bearer $DEPLOY_TOKEN" \
            -d '{"catalog": "https://catalog.example.com/assets/remoteEntry.js"}'

Microfrontend monitoring

// Track loading of each remote
window.addEventListener('unhandledrejection', (e) => {
  if (e.reason?.message?.includes('Loading chunk')) {
    monitoring.track('remote_load_failure', {
      error: e.reason.message,
      remote: detectRemoteFromStack(e.reason.stack),
    })
  }
})

// Web Vitals for each microfrontend
import { onLCP, onFID, onCLS } from 'web-vitals'
onLCP((metric) => analytics.track('LCP', { value: metric.value, remote: 'catalog' }))

Common mistakes during transition

Too fine-grained division — a microfrontend from 3 components is not justified by operational overhead.

No contracts — teams start depending on each other's internal implementations.

Unsynchronized versions of shared dependencies — two React on page → two VDOM, hook bugs, bloated bundle.

Direct imports between microfrontends — violates isolation and makes independent deployment impossible.

What we do

We conduct domain boundary analysis with product/engineering teams, select the integration approach, design shell and contracts package, set up Module Federation or Single-SPA, organize CI/CD with independent deployment, configure remote-loading monitoring.

Timeline: 10–20 days — architecture, shell implementation, setup 2–3 microfrontends, CI/CD. Further logic migration — within separate tasks.