← Back to Micro Frontend
Lesson 1 · Micro Frontend Fundamentals

What is Micro Frontend?

One web page, built from pieces that different teams build and release on their own. See it work: a shell page loads three pieces from three servers at runtime, one team releases a new version without the others, and one piece goes down while the page stays up - all tested in a real browser.

Beginner25 min

What you will be able to do

  • Explain micro frontend in one sentence
  • Name the parts: shell (host) and pieces (remotes)
  • See runtime integration work with plain JavaScript modules
  • See one piece released without touching the others
  • See the page survive when one piece fails
  • Tell what is NOT a micro frontend

The idea, in plain English

A normal web app is built and released as one thing. A micro frontend app looks like one page to the user, but behind it are several smaller frontends, each owned by one team, each built and released on its own. A small "shell" page puts them together in the browser.

The idea comes from microservices on the server side: split a big system so teams can work and release independently. Micro frontend applies the same split to what runs in the browser. The pieces can live on different servers, be released on different days - even be written with different tools - as long as they follow a small contract with the shell.

In this lesson we build the smallest real example with no framework at all: four tiny Node servers (one per team), plain JavaScript modules, and a browser. Later modules replace the plain modules with Module Federation and React. Node 22, Chromium through Playwright.

Worked example: A book shop page assembled from three separately released builds - catalog, cart and reviews - each on its own server.

Words you will see in this course

These words come up in almost every lesson.

Small dictionary
FrontendThe part of an app that runs in the browser - what the user sees and clicks.
Micro frontendA part of the frontend that one team builds and releases on its own.
Shell (host)The page that loads the pieces and puts them together. Also called the container.
RemoteA piece the shell loads from somewhere else at runtime.
Release (deploy)Putting a new version where users get it.
RuntimeWhile the page is running in the browser - not when it is built.
ContractWhat the shell and a piece agree on - here: "export a mount(element) function".

An everyday example: a shopping mall

  • To a visitor, a mall is one place: one entrance, one roof, one map.
  • Inside, each shop is run by its own owner. A shop can repaint, change its stock or close for a day without asking the others.
  • The mall itself owns the building, the corridors and the signs - that is the shell.
  • The shops agree to some rules: opening hours, the size of the sign - that is the contract.
  • A frontend monolith is one big department store: one owner decides everything, and a change on one floor waits for the whole store’s plan.

Example 1 - four servers, one page

Four folders, four teams. serve.mjs is a tiny file server; we start one copy per folder, each on its own port:

  • shell (port 4100) - index.html with three empty slots, and shell.js.
  • catalog (4101), cart (4102), reviews (4103) - one file each, owned by one team.
  • serve.mjs sends Access-Control-Allow-Origin: * because the shell runs on another port, and Cache-Control: no-cache so the browser asks for the newest release.
serve.mjs
// A tiny static server: one per team, each on its own port. import { createServer } from 'node:http'; import { readFile } from 'node:fs/promises'; import { extname, join } from 'node:path'; const [folder, port] = process.argv.slice(2); const types = { '.html': 'text/html', '.js': 'text/javascript' }; createServer(async (req, res) => { const path = join(folder, req.url === '/' ? 'index.html' : req.url); try { const body = await readFile(path); res.writeHead(200, { 'Content-Type': types[extname(path)] ?? 'text/plain', 'Access-Control-Allow-Origin': '*', // the shell lives on another port 'Cache-Control': 'no-cache', // always ask for the newest release }); res.end(body); } catch { res.writeHead(404).end('not found'); } }).listen(port, () => console.log(`${folder} on http://localhost:${port}`));
Start the four servers (four terminals)
node serve.mjs shell 4100 node serve.mjs catalog 4101 node serve.mjs cart 4102 node serve.mjs reviews 4103

Example 2 - the shell and the pieces

The shell does not contain any shop code. It only knows where each piece lives:

  • pieces maps each slot to a URL on another server.
  • await import(url) loads a JavaScript module at runtime - the browser fetches it like any file.
  • Every piece follows the same contract: it exports mount(element) and draws itself into that element.
  • try / catch: if a piece cannot be loaded, the shell writes a short message into that slot and goes on.
  • Each piece is one small file owned by one team. In real life it would be a whole React or Vue app - the contract stays the same.
shell/index.html
<!doctype html> <html> <head><title>Book shop</title></head> <body> <header>Book shop - the shell, version 1</header> <main> <section id="catalog">loading...</section> <section id="cart">loading...</section> <section id="reviews">loading...</section> </main> <script type="module" src="./shell.js"></script> </body> </html>
shell/shell.js
// The shell only knows WHERE each piece lives - not how it is built. const pieces = { catalog: 'http://localhost:4101/catalog.js', cart: 'http://localhost:4102/cart.js', reviews: 'http://localhost:4103/reviews.js', }; for (const [id, url] of Object.entries(pieces)) { const slot = document.getElementById(id); try { const piece = await import(url); // loaded at runtime, from another server piece.mount(slot); } catch { slot.textContent = `${id} is not available right now`; // one piece down, the page stays up } }
catalog/catalog.js, cart/cart.js, reviews/reviews.js
// Team Products owns this file and releases it on its own. export function mount(element) { element.textContent = 'Catalog v1: 3 books'; } // Team Checkout owns this file and releases it on its own. export function mount(element) { element.textContent = 'Cart v1: 0 items'; } // Team Community owns this file and releases it on its own. export function mount(element) { element.textContent = 'Reviews v1: 12 reviews'; }

Example 3 - three tests in a real browser

visit.js drives Chromium with Playwright and does what a user and the teams would do:

  • 1. First visit - the page showed all three pieces. The browser loaded 5 files from 4 different servers.
  • 2. Team Checkout changed ONE file, cart/cart.js, and "released" it. After a reload the page showed Cart v2. The shell files had the same SHA-1 hashes as before - nobody rebuilt or redeployed the shell.
  • 3. We stopped the reviews server. The import failed, the shell wrote "reviews is not available right now" in that slot, and catalog and cart kept working.
visit.js
const { chromium } = require('playwright'); const { execSync } = require('child_process'); const fs = require('fs'); const read = async (p) => ({ catalog: await p.innerText('#catalog'), cart: await p.innerText('#cart'), reviews: await p.innerText('#reviews'), }); const shellHash = () => execSync('shasum shell/index.html shell/shell.js').toString().trim().split('\n').map((l) => l.slice(0, 8)).join(' '); (async () => { const b = await chromium.launch(); const p = await b.newPage(); const files = []; p.on('response', (r) => files.push(`${r.status()} ${r.url()}`)); console.log('--- 1. First visit'); await p.goto('http://localhost:4100/', { waitUntil: 'networkidle' }); console.log(await read(p)); console.log('files the browser loaded:'); files.forEach((f) => console.log(' ' + f)); console.log('shell files (sha1):', shellHash()); console.log('--- 2. Team Checkout releases cart v2 (only cart/cart.js changes)'); fs.writeFileSync('cart/cart.js', fs.readFileSync('cart/cart.js', 'utf8').replace("'Cart v1: 0 items'", "'Cart v2: 0 items - now with gift wrap'")); await p.reload({ waitUntil: 'networkidle' }); console.log(await read(p)); console.log('shell files (sha1):', shellHash()); console.log('--- 3. The reviews server goes down'); execSync("pkill -f 'node serve.mjs reviews'"); await p.waitForTimeout(300); await p.reload({ waitUntil: 'networkidle' }); console.log(await read(p)); await b.close(); })();
Output
--- 1. First visit { catalog: 'Catalog v1: 3 books', cart: 'Cart v1: 0 items', reviews: 'Reviews v1: 12 reviews' } files the browser loaded: 200 http://localhost:4100/ 200 http://localhost:4100/shell.js 200 http://localhost:4101/catalog.js 200 http://localhost:4102/cart.js 200 http://localhost:4103/reviews.js shell files (sha1): d8fd2a57 ef02fca0 --- 2. Team Checkout releases cart v2 (only cart/cart.js changes) { catalog: 'Catalog v1: 3 books', cart: 'Cart v2: 0 items - now with gift wrap', reviews: 'Reviews v1: 12 reviews' } shell files (sha1): d8fd2a57 ef02fca0 --- 3. The reviews server goes down { catalog: 'Catalog v1: 3 books', cart: 'Cart v2: 0 items - now with gift wrap', reviews: 'reviews is not available right now' }

What makes it "micro frontend"

Three things together - not just "the code is in several files":

  • Independent release - a team ships its piece without waiting for or rebuilding the others (step 2).
  • Put together late - at runtime in the browser (here), on the server, or at build time. Lesson 8 compares the three.
  • Owned by one team each - a piece matches a part of the business (catalog, cart), not a technical layer.
  • Bonus, if you build it well: one broken piece does not break the page (step 3).

What it is NOT

  • Components in one repository, built and released together - that is a well-organised monolith, not micro frontend.
  • An npm package of shared buttons - a library. Every app that uses it must rebuild to get a new version.
  • Separate websites with links between them - the user sees separate sites, not one page.
  • Micro frontend is about how TEAMS release, not about how small the files are. Lesson 2 explains why that matters.

The idea at a glance

Shell

Loads and places the pieces.

const piece = await import(url)
Contract

What every piece offers.

export function mount(element) { ... }
Fallback

One piece down, page up.

catch { slot.textContent = "... not available" }
Independent release

Only that team’s file changes.

cart/cart.js -> v2, shell unchanged

Try it yourself

The code does not change. Swap the content string and the program does something else entirely.

Run it

“Create the four folders, start the four servers, and open http://localhost:4100. Open the Network tab - which servers do the files come from?”

Release a piece

“Change the text in catalog.js and reload. Did you touch any other folder?”

Break the contract

“Rename mount to render in reviews.js. What does the page show, and why?”

A fourth team

“Add a "recommendations" piece on port 4104. What did you have to change in the shell?”

What usually goes wrong

Calling a shared component library "micro frontend"

If every app must rebuild to get the new version, the release is not independent.

No fallback in the shell

Without try / catch, one failing piece can stop the rest from loading.

Forgetting the contract

The shell and every piece must agree on what is exported. Write it down.

Splitting by file size instead of by team

Micro frontend is about who releases what. Many small pieces owned by one team give you the cost without the benefit.

Practice

Write these yourself before opening anything. Getting them wrong first is most of how this sticks.

1.

Add a fourth piece, "recommendations", owned by a new team on port 4104. It should show "You may also like: 2 books". Then make it slow on purpose (serve.mjs waits 3 seconds before answering for that file only) and change the shell so the other pieces appear at once instead of waiting for it.

Show hint

The for loop awaits each import one after another. Start all imports first, for example with Promise.allSettled, or load each piece in its own async function without awaiting the loop.

Key points

  • A micro frontend app is one page for the user, many separately released pieces behind it.
  • The shell knows where pieces live and puts them together; the pieces follow a small contract.
  • Runtime integration: the browser loads pieces with import() while the page runs.
  • A team releases its piece without rebuilding the shell or the other pieces.
  • Built well, one broken piece does not break the page.
  • It is about team independence, not about small files.

Quick check before you move on

How many servers did the browser load files from?
Four: the shell and three pieces.
What changed when Checkout released cart v2?
Only cart/cart.js. The shell files kept the same hashes.
What did the page show when the reviews server was down?
"reviews is not available right now" in that slot - catalog and cart still worked.
What is the contract between the shell and a piece here?
Every piece exports a mount(element) function.

Interview questions

What is a micro frontend?

An architectural style where a web app is composed of independently developed and deployed frontend pieces, each owned by a team aligned to a business domain, integrated by a shell at build time, on the server, or at runtime in the browser.

What is the role of the application shell?

It owns cross-cutting concerns - layout, routing, session, loading and error isolation of remotes - and knows where to find each piece through a contract, without containing domain code itself.

Quiz

  1. 1.

    Why does serve.mjs send Access-Control-Allow-Origin?

  2. 2.

    Is a shared npm package of buttons a micro frontend? Why?

  3. 3.

    Which three things together make something a micro frontend?

Comments

Sign in to leave a comment. Your name and photo come from Google; nothing else is shared.

Loading comments...