DEV Community

Cover image for Microfrontends com Next.js: guia completo e exemplo com 3 apps
Anderson Nascimento
Anderson Nascimento

Posted on

Microfrontends com Next.js: guia completo e exemplo com 3 apps

O que são microfrontends — e por que usar

Microfrontends aplicam os princípios de microserviços ao front-end: você divide a interface em módulos/autônomos (MFEs) que podem ser desenvolvidos, versionados, implantados e escalados de forma independente. O “app raiz” (host/shell) orquestra e compõe esses módulos em tempo de execução.

Benefícios

  • Times autônomos e ciclos de release independentes.
  • Escalabilidade organizacional: cada MFE tem seu backlog, deploy e métricas.
  • Evolução tecnológica incremental (ex.: um MFE pode migrar biblioteca sem travar o restante).
  • Foco por domínio (ex.: Catálogo, Checkout, Conta, etc.).
  • Deploys menores e rollback rápido.

Quando faz sentido (e quando não)

Use quando:

  • O produto é grande e de longa duração, com múltiplos domínios e vários times.
  • Você precisa desacoplar releases e reduzir impacto de mudanças.
  • migração gradual de uma base legada.

Evite se:

  • O time é pequeno, o app é simples, ou o custo de observabilidade, design system e coordenação supera os ganhos.

A escolha técnica deste guia

Para habilitar microfrontends em Next.js, adotaremos o Module Federation, recurso nativo do Webpack que permite compartilhar módulos entre diferentes aplicações em tempo de execução.

Faremos essa integração por meio do plugin @module-federation/nextjs-mf, que atualmente oferece suporte apenas ao Pages Router. Até o momento, o App Router não é suportado oficialmente — essa limitação está documentada no próprio repositório do projeto.

Outro ponto importante: o Turbopack (o novo bundler do Next.js) ainda não possui suporte ao Module Federation. Por esse motivo, neste guia continuaremos utilizando o Webpack, garantindo compatibilidade com a abordagem proposta. Module FederationGitHub


Versões recomendadas (compatíveis entre si)

Para evitar surpresas, fixaremos versões estáveis já conhecidas por funcionarem bem juntas:

  • Node.js: 20.x LTS (amplamente suportado hoje; 18.x já está em EoL).

    Node 18 entrou em EoL em 27/03/2025; 20.x está em LTS de manutenção. Node.js

  • Next.js: 13.4.19 (Pages Router, Webpack 5; compatível com o plugin e sem App Router).

  • React / React-DOM: 18.2.0.

  • Module Federation (Next plugin): @module-federation/nextjs-mf@8.8.38 (ou a última 8.x estável). npm

  • Webpack: 5.x como dependência do projeto (o plugin recomenda usar o “local webpack”). Module Federation

  • Gerenciador de pacotes: Yarn (v1.x).

Por que Next 13.4.19? Porque apesar do plugin documentar suporte, o App Router não é suportado; com Pages Router do 13.4.x você obtém estabilidade.


O que vamos construir

Três projetos independentes:

  1. root-shell (host) — orquestra e consome dois MFEs.
  2. mfe1 — expõe um Header.
  3. mfe2 — expõe um ProductCard.

Portas fixas para padronizar os comandos:

  • root-shell: 3000
  • mfe1: 3001
  • mfe2: 3002

Passo a passo (na ordem): Root → MFE1 → MFE2

Pré-requisitos

  • Node 20.x ativo (nvm use 20).
  • Yarn 1.x.
  • (Opcional) .nvmrc com 20 em cada projeto.

1) Criar o root-shell (host)

# 1. criar projeto base
yarn create next-app root-shell --typescript

cd root-shell

# 2. Fixar versões
yarn add -E next@13.4.19 react@18.2.0 react-dom@18.2.0

# 3. Module Federation plugin + webpack local + utilitários
yarn add -D @module-federation/nextjs-mf@8.8.38 webpack@^5 cross-env

# 4. Criar diretório types
mkdir -p src/types

Enter fullscreen mode Exit fullscreen mode

O plugin orienta ativar NEXT_PRIVATE_LOCAL_WEBPACK=true e ter webpack instalado no projeto. Module Federation

package.json (trecho): defina as portas e a flag de “local webpack” (recomendada pelo plugin).

{
  "name": "root-shell",
  "private": true,
  "scripts": {
    "dev": "cross-env NEXT_PRIVATE_LOCAL_WEBPACK=true next dev -p 3000",
    "build": "cross-env NEXT_PRIVATE_LOCAL_WEBPACK=true next build",
    "start": "next start -p 3000"
  }
}

Enter fullscreen mode Exit fullscreen mode

Renomear o arquivo next.config

mv next.config.ts next.config.js
Enter fullscreen mode Exit fullscreen mode

next.config.js: configurar o host consumindo remotes mfe1 e mfe2.

// eslint-disable-next-line @typescript-eslint/no-require-imports
const NextFederationPlugin = require('@module-federation/nextjs-mf');

module.exports = {
  reactStrictMode: true,
  webpack(config) {
    config.plugins.push(
      new NextFederationPlugin({
        name: 'root',
        filename: 'static/chunks/remoteEntry.js',
        remotes: {
          mfe1: `mfe1@http://localhost:3001/_next/static/chunks/remoteEntry.js`,
          mfe2: `mfe2@http://localhost:3002/_next/static/chunks/remoteEntry.js`
        },
        shared: {},
        extraOptions: {
          exposePages: false
        }
      })
    );
    return config;
  }
};

Enter fullscreen mode Exit fullscreen mode

src/types/global.d.ts (tipos para os módulos remotos):

declare module 'mfe1/header' {
  const Header: React.ComponentType<{ title?: string }>;
  export default Header;
}

declare module 'mfe2/product-card' {
  const ProductCard: React.ComponentType<{ name: string; price: number }>;
  export default ProductCard;
}

Enter fullscreen mode Exit fullscreen mode

src/pages/_app.tsx (padrão Pages Router):

import type { AppProps } from 'next/app';

export default function App({ Component, pageProps }: AppProps) {
  return <Component {...pageProps} />;
}

Enter fullscreen mode Exit fullscreen mode

src/pages/index.tsx — O ssr: false evita Hydration mismatch ao renderizar componentes remotos no cliente.

import dynamic from "next/dynamic";

const Header = dynamic(() => import("mfe1/header"), {
  ssr: false,
  loading: () => <div>Carregando Header…</div>
});

const ProductCard = dynamic(() => import("mfe2/product-card"), {
  ssr: false,
  loading: () => <div>Carregando Produto…</div>
});

export default function Home() {
  return (
    <>
      <Header title="Microfrontends com Next.js" />

      <main style={{ padding: 24 }}>
        <h2>Vitrine</h2>
        <ProductCard name="Camiseta Dev" price={99.9} />
      </main>
    </>
  );
}

Enter fullscreen mode Exit fullscreen mode

2) Criar o mfe1 (remote com Header)

# 1. criar mfe1
yarn create next-app mfe1 --typescript

cd mfe1

# 2. Fixar versões
yarn add -E next@13.4.19 react@18.2.0 react-dom@18.2.0

# 3. Module Federation plugin + webpack local + utilitários
yarn add -D @module-federation/nextjs-mf@8.8.38 webpack@^5 cross-env

# 4. Criar diretório components
mkdir -p src/components

Enter fullscreen mode Exit fullscreen mode

package.json (trecho):

{
  "name": "mfe1",
  "private": true,
  "scripts": {
    "dev": "cross-env NEXT_PRIVATE_LOCAL_WEBPACK=true next dev -p 3001",
    "build": "cross-env NEXT_PRIVATE_LOCAL_WEBPACK=true next build",
    "start": "next start -p 3001"
  }
}

Enter fullscreen mode Exit fullscreen mode

src/components/Header.tsx:

type Props = { title?: string };

export default function Header({ title = 'MFE1 Header' }: Props) {
  return (
    <header style={{ padding: 16, background: '#111', color: '#fff' }}>
      <h1 style={{ margin: 0, fontSize: 20 }}>{title}</h1>
    </header>
  );
}

Enter fullscreen mode Exit fullscreen mode

Renomear o arquivo next.config

mv next.config.ts next.config.js
Enter fullscreen mode Exit fullscreen mode

next.config.js — expor o componente:

// eslint-disable-next-line @typescript-eslint/no-require-imports
const NextFederationPlugin = require('@module-federation/nextjs-mf');

module.exports = {
  webpack(config) {
    config.plugins.push(
      new NextFederationPlugin({
        name: 'mfe1',
        filename: 'static/chunks/remoteEntry.js',
        exposes: {
          './header': './src/components/Header.tsx'
        },
        shared: {},
        extraOptions: {
          exposePages: false,
          ssr: false
        }
      })
    );
    return config;
  }
};

Enter fullscreen mode Exit fullscreen mode

src/pages/index.tsx (para ver o MFE isolado):

import Header from '../components/Header';

export default function Index() {
  return (
    <><Header title="MFE1 isolado" />
      <p style={{ padding: 16 }}>Este é o MFE1 rodando sozinho.</p>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

3) Criar o mfe2 (remote com ProductCard)


# 1. criar mfe2
yarn create next-app mfe2 --typescript

cd mfe2

# 2. Fixar versões
yarn add -E next@13.4.19 react@18.2.0 react-dom@18.2.0

# 3. Module Federation plugin + webpack local + utilitários
yarn add -D @module-federation/nextjs-mf@8.8.38 webpack@^5 cross-env

# 4. Criar diretório components
mkdir -p src/components
Enter fullscreen mode Exit fullscreen mode

package.json (trecho):

{
  "name": "mfe2",
  "private": true,
  "scripts": {
    "dev": "cross-env NEXT_PRIVATE_LOCAL_WEBPACK=true next dev -p 3002",
    "build": "cross-env NEXT_PRIVATE_LOCAL_WEBPACK=true next build",
    "start": "next start -p 3002"
  }
}
Enter fullscreen mode Exit fullscreen mode

src/components/ProductCard.tsx:

type Props = { name: string; price: number };

export default function ProductCard({ name, price }: Props) {
  return (
    <article style={{ border: '1px solid #ddd', borderRadius: 8, padding: 16, width: 280 }}>
      <strong style={{ display: 'block', marginBottom: 8 }}>{name}</strong>
      <span>R$ {price.toFixed(2)}</span>
    </article>
  );
}
Enter fullscreen mode Exit fullscreen mode

Renomear o arquivo next.config

mv next.config.ts next.config.js
Enter fullscreen mode Exit fullscreen mode

next.config.js — expor o componente:

// eslint-disable-next-line @typescript-eslint/no-require-imports
const NextFederationPlugin = require('@module-federation/nextjs-mf');

module.exports = {
  webpack(config) {
    config.plugins.push(
      new NextFederationPlugin({
        name: 'mfe2',
        filename: 'static/chunks/remoteEntry.js',
        exposes: {
          './product-card': './src/components/ProductCard.tsx'
        },
        shared: {},
        extraOptions: {
          exposePages: false,
          ssr: false
        }
      })
    );
    return config;
  }
};

Enter fullscreen mode Exit fullscreen mode

src/pages/index.tsx:

import ProductCard from '../components/ProductCard';

export default function Index() {
  return (
    <main style={{ padding: 16 }}>
      <h1>MFE2 isolado</h1>
      <ProductCard name="Livro MF" price={59.9} />
    </main>
  );
}

Enter fullscreen mode Exit fullscreen mode

Aviso ESLint sobre require()

No next.config.js dos projetos usamos:

// eslint-disable-next-line @typescript-eslint/no-require-imports
const NextFederationPlugin = require('@module-federation/nextjs-mf');
Enter fullscreen mode Exit fullscreen mode

Por que ocorre a warning?

O ESLint, com a regra @typescript-eslint/no-require-imports, recomenda usar import em vez de require() para manter consistência e suporte a tree-shaking.

Por que não há problema em desabilitar?

No contexto de Next.js + Module Federation, o plugin precisa ser importado via require(), porque é usado diretamente na configuração do Webpack, que ainda depende do CommonJS.

Sobre tipos TypeScript para MFEs

Para que o TypeScript “entenda” os imports das rotas remotas ('mfe1/header', 'mfe2/product-card'), criamos declarações em src/types/global.d.ts no host (e, se necessário, nos remotes que consumirem outros remotes). Esse arquivo evita erros de “módulo não encontrado” e centraliza a organização dos tipos.


Como rodar os três projetos (ordem recomendada)

1) Inicie os remotes primeiro (para que o host encontre os remoteEntry.js).

2) Depois suba o host.

No MFE1:

cd mfe1
yarn dev   # http://localhost:3001
Enter fullscreen mode Exit fullscreen mode

No MFE2:

cd mfe2
yarn dev   # http://localhost:3002
Enter fullscreen mode Exit fullscreen mode

No Root (host):

cd root-shell
yarn dev   # http://localhost:3000

Enter fullscreen mode Exit fullscreen mode

Acesse http://localhost:3000 e você verá o Header (MFE1) e o ProductCard (MFE2) renderizados dentro do host.

Produção: use yarn build em cada app. Depois, yarn start -p mantendo as mesmas portas ou atualize os URLs de remotes para o domínio real (CDN/host de produção).


Por que congelamos versões — e a questão do next.config

  • create-next-app instala a versão mais recente por padrão. Para evitar cair numa combinação Next (recente) + App Router + Turbopack — que não funciona com MF — forçamos next@13.4.19 (Pages Router). Module Federation
  • next.config.js: os exemplos do plugin usam JS e funcionam muito bem no 13.4.x; já o next.config.ts passou a ser suportado oficialmente só no Next 15. Como estamos no 13.4.x, manteremos JS para compatibilidade máxima. nextjs.org

Dicas e troubleshooting

  • 404 em remoteEntry.js: confirme as portas e os caminhos /_next/static/ssr|chunks/remoteEntry.js no next.config.js do host de acordo com isServer. Module Federation
  • Versões de Node: se você ainda precisa de Node 18 em dev, saiba que está em EoL; considere planejar migração para 20.x LTS. Node.js
  • Local Webpack: se aparecer erro relacionado a MF/webpack, verifique se a flag NEXT_PRIVATE_LOCAL_WEBPACK está ativa e webpack@5 está no devDependencies. Module Federation

Conclusão

Com Module Federation + Next (Pages Router) você obtém microfrontends de verdade: MFEs independentes compondo uma experiência única no host, com autonomia de times e deploys desacoplados. O segredo está em congelar versões compatíveis, por enquanto ficar no Pages Router, usar Webpack 5 (não Turbopack) e configurar os remotes com cuidado.

Top comments (0)