Microfrontends na Prática - Construindo seu Primeiro Projeto do Zero
Profile picture

Junior Alves

Senior Developer

Foto: Unsplash

Atualizado: 14 de julho de 2025 às 08:39

Leitura: 8 minutos de leitura

Criado: 10 de julho de 2025

Microfrontends na Prática - Construindo seu Primeiro Projeto do Zero

Um guia passo a passo para criar um Shell e um Remote com Module Federation, TypeScript e RSBuild.

Introdução

Você já ouviu falar de microfrontends? Talvez já tenha lido sobre como eles prometem resolver as dores de cabeça dos grandes projetos monolíticos, trazendo autonomia para os times e agilidade para os deploys. A teoria é fantástica, mas muitas vezes o que falta é um exemplo prático, um "Hello World" que nos mostre como as peças realmente se encaixam.

É exatamente isso que faremos hoje.

Neste post, vamos colocar a mão no código e construir, do zero, uma aplicação com microfrontends usando um setup com TypeScript, pnpm e RSBuild (um build tool de alta performance que usa Rspack/Webpack por baixo dos panos). Ao final, você terá:

  • Um Shell (ou Host), nossa aplicação principal.
  • Um Microfrontend (ou Remote), uma aplicação independente.
  • Ambos se comunicando e compartilhando dependências em tempo de execução com a mágica do Module Federation.

Este guia é para você, dev frontend que quer dar o próximo passo, na prática.

Refrescando a Memória

Antes de codificar, vamos alinhar dois conceitos-chave:

  1. A Arquitetura: Pense em microfrontends como "Lego". Em vez de uma única peça gigante e imutável (o monolito), temos vários blocos menores e independentes (o MFE de busca, o MFE de perfil, etc.). O Shell é a base onde encaixamos esses blocos.
  2. A Tecnologia (Module Federation): É o que permite que essa "montagem de Lego" aconteça dinamicamente no navegador. Ele possibilita que uma aplicação (o Shell) carregue um módulo de outra aplicação em tempo de execução, e o mais importante, que elas compartilhem dependências (como o React) para não duplicar o código.

Com isso em mente, vamos ao que interessa.

Parte 1: Preparando o Terreno (Nosso Monorepo)

A forma mais "simples" de trabalhar é com um monorepo. Vamos criar uma estrutura de pastas simples.

# Crie a pasta principal do projeto
mkdir my-microfrontend
cd my-microfrontend
 
# Crie a pasta que conterá nossos projetos
mkdir packages
cd packages
 
# Crie as pastas para o shell e nosso primeiro MFE
mkdir shell
mkdir mfe-calculator

Sua estrutura deve ser esta:

my-microfrontend/
└── packages/
    ├── mfe-calculator/
    └── shell/

Parte 2: Construindo o Orquestrador (O Shell/Host)

O Shell é a nossa aplicação principal, o "orquestrador" que irá carregar os outros.

1. Vá até a pasta e inicie o projeto:

cd packages/shell
pnpm init

2. Instale as dependências:

# Dependências de desenvolvimento (RSBuild e tipos)
pnpm add -D @rsbuild/core @rsbuild/plugin-react @types/react @types/react-dom @types/node
 
# Dependências do React
pnpm add react react-dom

3. Crie os arquivos de configuração e código:

Crie os arquivos rsbuild.config.mjs, tsconfig.json, index.html e a pasta src com index.tsx e App.tsx dentro.

rsbuild.config.mjs

Este é o cérebro do nosso Shell.

import { defineConfig } from '@rsbuild/core';
import { pluginReact } from '@rsbuild/plugin-react';
 
export default defineConfig({
  plugins: [pluginReact()],
  server: {
    port: 3000, // O Shell rodará na porta 3000
  },
  moduleFederation: {
    name: 'shell',
    // Define quais MFEs este Shell pode carregar
    remotes: {
      'mfe_frete': 'mfe_frete@http://localhost:3001/remoteEntry.js',
    },
    // Define as dependências que serão compartilhadas
    shared: {
      react: { singleton: true },
      'react-dom': { singleton: true },
    },
  },
});

tsconfig.json

{
  "compilerOptions": {
    "target": "ESNext",
    "lib": ["DOM", "DOM.Iterable", "ESNext"],
    "module": "ESNext",
    "moduleResolution": "bundler",
    "jsx": "react-jsx",
    "strict": true,
    "esModuleInterop": true
  },
  "include": ["src"]
}

src/App.tsx

Por enquanto, nosso App será bem simples.

import React from 'react';
 
export const App = () => {
  return (
    <div style={{ border: '2px dashed navy', padding: '20px' }}>
      <h1>Host: Aplicação Principal (Shell)</h1>
      <hr />
      <h2>Área para o Microfrontend:</h2>
      {/* O MFE será renderizado aqui em breve */}
    </div>
  );
};

src/index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-g">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Shell</title>
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>

src/index.tsx

import { createRoot } from 'react-dom/client';
import { App } from './App';
 
const root = createRoot(document.getElementById('root')!);
root.render(<App />);

(Não se esqueça de criar o index.html e o src/index.tsx com o código padrão para renderizar o componente App).

4. Adicione o script de dev no package.json:

"scripts": {
  "dev": "rsbuild dev"
}

Parte 3: Criando Nosso Primeiro Microfrontend (O Remote)

Agora, vamos criar o mfe-calculator. O processo é quase idêntico.

1. Navegue para a pasta e instale as dependências:

cd ../mfe-calculator # Volte para a pasta packages e entre na mfe-calculator
pnpm init
pnpm add -D @rsbuild/core @rsbuild/plugin-react @types/react @types/react-dom @types/node
pnpm add react react-dom

2. Crie a mesma estrutura de arquivos: rsbuild.config.mjs, tsconfig.json, index.html, src/index.tsx e src/App.tsx. Além deles, crie o componente que vamos exportar: src/ShippingCalculator.tsx.

rsbuild.config.mjs

A configuração do Remote é um pouco diferente. Em vez de remotes, usamos exposes.

import { defineConfig } from '@rsbuild/core';
import { pluginReact } from '@rsbuild/plugin-react';
 
export default defineConfig({
  plugins: [pluginReact()],
  server: {
    port: 3001, // O MFE de frete rodará na porta 3001
  },
  moduleFederation: {
    name: 'mfe_calculator',
    // Define o que este MFE vai expor para o mundo
    exposes: {
      // O apelido './Calculator' aponta para o nosso componente
      './Calculator': './src/ShippingCalculator.tsx',
    },
    shared: {
      react: { singleton: true },
      'react-dom': { singleton: true },
    },
  },
});

src/ShippingCalculator.tsx

Este é o componente que o Shell irá importar.

import { useState } from 'react';
 
const ShippingCalculator = () => {
  const [cep, setCep] = useState('');
  
  return (
    <div style={{ border: '2px solid green', padding: '20px' }}>
      <h3>Remote: Componente de Frete</h3>
      <p>Este conteúdo veio da porta 3001!</p>
      <input 
        type="text" 
        placeholder="Digite o CEP"
        value={cep}
        onChange={(e) => setCep(e.target.value)}
      />
      <button onClick={() => alert(`Calculando para ${cep}...`)}>
        Calcular
      </button>
    </div>
  );
};
 
export default ShippingCalculator;
 

(Lembre-se de adicionar o script "dev": "rsbuild dev" no package.json deste projeto também).

Parte 4: A Mágica Acontece - Conectando Tudo

Agora, vamos fazer os dois projetos conversarem.

1. Inicie os dois servidores:

Abra dois terminais.

  • No terminal 1: cd packages/shell && pnpm dev
  • No terminal 2: cd packages/mfe-calculator && pnpm dev

2. Consuma o Componente no Shell:

Volte ao arquivo src/App.tsx do Shell e modifique-o para importar e renderizar o nosso MFE. Como o carregamento é assíncrono, usaremos React.lazy e Suspense.

// shell/src/App.tsx
import React, { Suspense } from 'react';
 
// Importa dinamicamente o componente exposto pelo nosso MFE
const RemoteCalculator = React.lazy(() => import('../../mfe-calculator/src/ShippingCalculator'));
 
export const App = () => {
  return (
    <div style={{ border: '2px dashed navy', padding: '20px' }}>
      <h1>Host: Aplicação Principal (Shell)</h1>
      <hr />
      <h2>Área para o Microfrontend:</h2>
      <Suspense fallback={<div>Carregando...</div>}>
        <RemoteCalculator />
      </Suspense>
    </div>
  );
};

Abra seu navegador em http://localhost:3000. Você deverá ver o seu Shell renderizando perfeitamente o componente que está sendo servido pela porta 3001!

Seção Bônus: Resolvendo o Erro Invalid hook call

É muito provável que, ao rodar o projeto acima, você se depare com um erro sobre "Invalid hook call" e "múltiplas cópias do React" (no console do browser). Isso não é um erro seu, mas um efeito colateral de como o pnpm usa links simbólicos em um monorepo.

Imagem mostrando erro no console

A solução é ser mais explícito na configuração do Module Federation, forçando a resolução para um caminho único.

A correção definitiva (aplique nos dois arquivos rsbuild.config.mjs):

import { defineConfig } from '@rsbuild/core';
import { pluginReact } from '@rsbuild/plugin-react';
import deps from './package.json' with { type: 'json' }; // Importe as dependências
import path from 'path'; // Adicione o import do 'path'
 
export default defineConfig({
  // ... plugins e server ...
 
  // Adicione esta seção 'tools'
   tools: {
    rspack(config) {
      config.resolve = config.resolve || {};
      config.resolve.alias = config.resolve.alias || {};
      config.resolve.alias.react = path.resolve('./node_modules/react');
      config.resolve.alias['react-dom'] = path.resolve('./node_modules/react-dom');
      return config;
    },
  },
  moduleFederation: {
    // ... name, remotes/exposes ...
    // Vamos melhorar um pouco nosso shared...
    shared: {
      react: {
        singleton: true,
        requiredVersion: deps.dependencies.react,
        eager: true,
      },
      'react-dom': {
        singleton: true,
        requiredVersion: deps.dependencies['react-dom'],
        eager: true,
      },
    },
  },
});

Testando seu entendimento

No arquivo de configuração do Module Federation de uma aplicação "Shell" (Host), qual propriedade é usada para declarar os microfrontends que ela pode carregar?

1
2// shell/rsbuild.config.mjs
3moduleFederation: {
4  name: 'shell',
5  // ???
6}
7        
1 / 5

Após aplicar essa correção, delete as pastas node_modules e o pnpm-lock.yaml, rode pnpm install na raiz e inicie os servidores novamente. O erro desaparecerá.

Conclusão

Parabéns! Você construiu e debugou uma aplicação com microfrontends. Você viu na prática como o Shell e o Remote são independentes, como o contrato é definido via remoteEntry.js e como resolver um dos problemas mais comuns do mundo real.

A partir daqui, o céu é o limite. Você pode experimentar adicionar um novo MFE, implementar roteamento entre eles ou explorar estratégias de deploy para um bucket na nuvem.

Espero que este guia prático tenha desmistificado o processo e te dado a confiança para explorar mais os microfrontends.

Curtiu? Compartilhe esse post:

Todos os direitos reseverdos © Junior Alves 2025