27 de dez. de 2025·8 min de leitura

Corrigir quebra de dependência ESM vs CommonJS em apps Node

Corrija quebras ESM vs CommonJS identificando rapidamente incompatibilidades de módulo e escolhendo a correção certa no package.json, build ou dependência.

Corrigir quebra de dependência ESM vs CommonJS em apps Node

Como é a quebra entre ESM e CommonJS

O Node.js tem duas maneiras de carregar arquivos JavaScript. CommonJS é o estilo mais antigo que usa require() e module.exports. ESM (ECMAScript Modules) é o estilo mais novo que usa import e export.

A maioria dos apps não é “pura” de um jeito só. Seu código pode ser CommonJS, uma dependência pode ser somente ESM, e sua ferramenta de desenvolvimento pode reescrever imports enquanto você roda localmente. Essa mistura é onde os problemas aparecem.

Quando alguém fala em “mismatch de formato de módulo”, quer dizer que um arquivo está sendo carregado como se fosse CommonJS, mas na verdade é ESM (ou o contrário). Por exemplo: seu código faz require('some-lib'), mas some-lib é só ESM, então o Node se recusa a carregá-lo com require().

É por isso que muitas quebras aparecem logo depois de adicionar uma dependência ou após o deploy. Muitos setups de dev escondem o mismatch:

  • TypeScript + ts-node/tsx pode executar imports no estilo ESM em dev mesmo que o output buildado seja CommonJS.
  • Bundlers podem fazer tudo funcionar localmente ao agrupar dependências, enquanto a produção usa a resolução do Node sem bundle.
  • Um build serverless ou Docker pode usar uma versão diferente do Node ou um arquivo de entrada diferente do que você roda localmente.

Um cenário típico: um protótipo roda bem com npm run dev, depois falha em produção logo após você adicionar um pequeno pacote utilitário. Localmente, o servidor de dev transpila tudo sob demanda. Em produção, você executa o dist/index.js compilado, o Node o trata como CommonJS, e o primeiro require() de uma dependência ESM-only lança erro.

Isso atinge muitas equipes que usam TypeScript, ferramentas no estilo Next/Nuxt e starters gerados por IA. Código gerado tende a misturar sinais (por exemplo, "type": "module" no package.json, mas saída CommonJS do build), o que cria falhas de carregamento “funciona na minha máquina”. O problema central é simples: o runtime e o output do build não estão falando a mesma linguagem de módulos.

Mensagens de erro comuns e o que elas geralmente indicam

Quando um app Node mistura ESM e CommonJS, a mensagem de crash costuma ser a pista mais rápida. A redação normalmente indica o que o Node pensou que o arquivo atual era (ESM ou CJS) e o que tentou carregar.

ERR_REQUIRE_ESM

Isso acontece quando código CommonJS chama require() em um pacote que é ESM-only.

Você geralmente vê isso depois de atualizar uma dependência, porque muitas bibliotecas migraram para ESM em versões principais. Gatilhos comuns incluem: seu arquivo sendo tratado como CommonJS (sem "type": "module", ou o arquivo termina em .cjs), requerer um caminho profundo que contorna os pontos de entrada do pacote, ou uma ferramenta (test runner, carregador de config) executando seu código em CommonJS mesmo que seu app seja em grande parte ESM.

O que o erro realmente diz: pare de usar require() para esse import, ou escolha uma versão da dependência que ainda suporte CommonJS.

“Cannot use import statement outside a module”

É a imagem espelhada. O Node está tratando o arquivo atual como CommonJS, mas ele contém sintaxe ESM como import.

Causas comuns incluem faltar "type": "module" no package.json, usar .js onde o Node espera .mjs, ou um passo de build que emite ESM enquanto seu runtime o inicia como CommonJS.

“Named export ... not found” (ou surpresas com export default)

Isso geralmente vem de assumir que as formas de exportação de ESM e CommonJS são iguais. Um módulo CommonJS costuma exportar um único objeto, enquanto ESM espera exports nomeados.

Um teste rápido: se import { thing } from "pkg" falha, mas import pkg from "pkg" funciona, você provavelmente está lidando com interoperabilidade CommonJS.

“exports is not defined” e surpresas de runtime semelhantes

Aparece quando código que espera globais do CommonJS (exports, require, module) roda em um contexto ESM que não os fornece.

Um padrão comum é “isso funcionou em dev”. Um servidor de dev transpila tudo, mas a produção roda os arquivos compilados diretamente. O output do build ainda contém exports.foo = ..., e o Node o carrega como ESM, então dá crash.

Verificações rápidas antes de mudar qualquer coisa

Ao bater em erros de formato de módulo, é tentador inverter "type": "module" ou começar a reescrever imports. Não faça isso ainda. Algumas checagens rápidas geralmente dizem se você puxou uma dependência ESM-only, se está iniciando o entry errado, ou se uma ferramenta está rodando seu código em um modo inesperado.

Comece confirmando o runtime exato. O mesmo código pode se comportar diferente sob node, ts-node, um test runner ou um bundler. Verifique também a versão do Node no ambiente que falha (local, CI, produção). Padrões e casos limítrofes mudaram entre versões do Node, e muitos provedores ficam atrás do que você tem localmente.

Antes de tocar no código, confirme:

  • A versão do Node e o comando de start em cada ambiente (por exemplo, node server.js vs um runner TypeScript).
  • O primeiro arquivo que lança e sua extensão: .cjs, .mjs, .js ou .ts.
  • O package.json mais próximo desse arquivo e se ele define "type": "module".
  • O nome da dependência e o caminho de arquivo mencionado na primeira linha relevante da stack trace.
  • Se acontece apenas em dev ou apenas em produção, e o que difere (output do build, método de instalação, variáveis de ambiente).

Dois padrões rápidos surgem sempre. Se a stack trace aponta para node_modules/<pkg>/... e diz que não pode ser require()-ado, você provavelmente puxou um pacote ESM-only em código CommonJS. Se aponta para seu output buildado (como dist/index.js), seu build e runtime discordam sobre o formato do módulo.

Um pequeno exemplo: um protótipo roda localmente via ts-node (que pode lidar com ESM de forma diferente), mas em produção roda node dist/server.js. Essa troca sozinha pode expor o mismatch que você precisa corrigir.

Passo a passo: diagnosticar o mismatch de formato de módulo

O caminho mais rápido é parar de adivinhar e identificar onde o Node pensa que a fronteira entre ESM e CommonJS está.

1) Comece pelo primeiro frame “do seu código”

Abra o erro e vá até a stack trace. Ignore a longa lista de frames dentro de node_modules no início. Encontre o primeiro frame que aponta para um arquivo seu (caminho do repo), e note o nome do arquivo e a extensão, a linha que disparou o carregamento (um import, require ou import() dinâmico), e o package.json mais próximo que controla esse arquivo.

Esse frame costuma ser onde a decisão errada de formato de módulo fica visível.

2) Confirme como o Node interpreta aquele arquivo (ESM ou CJS)

O Node decide ESM vs CJS principalmente por extensões de arquivo e package.json:

  • .mjs roda como ESM.
  • .cjs roda como CommonJS.
  • .js depende de type no package.json (se "type": "module", é ESM; caso contrário, CommonJS).

Um pegadinha comum: você pensa que está em CommonJS porque escreveu require(), mas seu pacote tem type: module, então o .js é na verdade ESM.

3) Inspecione os pontos de entrada da dependência

Olhe a dependência que está sendo carregada no ponto de falha. No node_modules/<pkg>/package.json, verifique o que o Node está selecionando:

  • main (geralmente CommonJS)
  • module (geralmente ESM, mas o Node nem sempre o usa diretamente)
  • exports (pode mapear arquivos diferentes para import vs require)

Se exports existir, ele frequentemente decide tudo. Um pacote pode exportar ESM para import mas não fornecer caminho CommonJS para require, o que leva a ERR_REQUIRE_ESM.

4) Reproduza com o menor snippet possível

Crie um arquivo mínimo ao lado do app (ou em uma pasta scratch) e teste só o import problemático.

// test-load.js
const pkg = require("the-problem-package");
console.log(pkg);

Depois tente a versão ESM também:

// test-load.mjs
import pkg from "the-problem-package";
console.log(pkg);

Se um funciona e o outro falha, você confirmou que é um mismatch de formato (não sua lógica de negócio).

5) Decida o que mudar: app, build ou dependência

Use o que aprendeu para escolher a correção de menor risco:

  • Mude o modo de módulo do app (extensões ou type) se você controlar a maior parte do código.
  • Mude o output do build/transpiler se você compilar TypeScript ou usar bundler.
  • Mude a dependência (fixe uma versão, troque o pacote ou use outro ponto de entrada) se o pacote não suporta mais seu formato.

Correções direcionadas no package.json (type, main, exports)

Corrija a diferença dev vs produção
Se funciona em dev mas falha no deploy, alinhamos a saída do build e o runtime.

Muitas quebras de formato de módulo podem ser resolvidas sem reescrever o código inteiro. O objetivo é deixar claro se seu pacote (ou dependência) é ESM, CommonJS ou ambos.

Comece por "type". Definir "type": "module" vira o padrão para todo .js naquele pacote ser ESM. Isso é ótimo se você está totalmente comprometido com ESM, mas também pode disparar uma cascata de falhas require(). Se você ainda tem arquivos CommonJS, considere deixar "type" vazio e optar por extensão por arquivo.

Quando você precisa de comportamentos diferentes por arquivo, prefira extensões em vez de switches globais:

  • Use .cjs para arquivos que precisam ser CommonJS (require, module.exports).
  • Use .mjs para arquivos que precisam ser ESM (import, export).
  • Use .js somente quando o default do pacote (type) corresponder ao que você pretende.

Em seguida, verifique seus pontos de entrada. "main" é a entrada clássica do Node e é geralmente CommonJS. Alguns bundlers também olham para "module" como entrada ESM. Se você precisa de ambos, aponte para arquivos diferentes (por exemplo dist/index.cjs vs dist/index.js).

"exports" é o mais poderoso e o que mais costuma surpreender. Uma vez presente, ele pode bloquear imports profundos como some-lib/dist/internal.js mesmo que esse arquivo exista. Ferramentas e test runners antigos também podem falhar se dependem de caminhos profundos. Use "exports" para expor só o que pretende, mas seja explícito sobre condições ESM e CommonJS.

Se estiver mudando pontos de entrada, evite quebrar consumidores: mantenha "main" estável enquanto introduz "exports", exporte alvos tanto para import quanto para require quando suportar ambos, e substitua caminhos profundos por exports públicos documentados.

Correções via configurações de build e transpile

Muitas falhas ESM/CommonJS não são realmente sobre a dependência. Vêm do output do build que não bate com a forma como o Node roda seu app.

Configurações TypeScript que decidem o que o Node vai carregar

TypeScript pode compilar código que parece correto no editor, mas os arquivos emitidos podem não corresponder ao seu runtime. Se você roda JavaScript compilado, verifique estas opções primeiro:

  • compilerOptions.module: CommonJS emite require(...); NodeNext ou ESNext emite import.
  • compilerOptions.moduleResolution: NodeNext entende regras ESM (como extensões de arquivo e exports).
  • compilerOptions.esModuleInterop e allowSyntheticDefaultImports: podem fazer imports compilar mesmo quando a interoperabilidade em runtime ainda está errada.
  • outDir: garanta que todo o código de runtime venha de uma pasta só (geralmente dist).

Uma regra simples: emita no mesmo formato de módulo que seu processo Node espera. Se seu app é ESM, emita ESM. Se é CommonJS, emita CommonJS.

Quando o bundler “conserta” em dev e depois o Node quebra

Bundlers e servidores de dev frequentemente reescrevem ou agrupam dependências, então o app parece funcionar durante o desenvolvimento. Depois, a produção roda Node puro contra seus arquivos compilados, e aí você bate em erros ESM/CJS.

Para reduzir surpresas, execute o comando de start de produção localmente contra o output compilado, não contra o servidor de dev.

Evite o mix src vs dist

Quebras voltam quando seu runtime importa alguns arquivos de src e outros de dist. Isso mistura sistemas de módulo e extensões.

Mantenha limpo:

  • Garanta que a produção rode só dist (ou só src, se você realmente roda TS diretamente).
  • Remova artefatos antigos do build antes de construir (arquivos obsoletos ainda podem ser importados).
  • Use caminhos de import consistentes que apontem para arquivos buildados.

Correções ajustando, fixando ou trocando dependências

Obtenha uma correção rápida
A maioria das correções é concluída em 48 a 72 horas, incluindo refatores que evitam regressões.

Às vezes a correção mais rápida está na escolha da dependência, não no seu código. Trate a dependência como a variável: escolha uma versão compatível, use um ponto de entrada suportado, ou troque por alternativa.

Troque por uma alternativa amigável ao CJS (quando possível)

Se seu app é CommonJS (usa require) e uma dependência virou ESM-only, trocar costuma ser mais limpo do que forçar um passo de build só para um pacote. Isso é especialmente verdadeiro para utilitários pequenos.

Ao escolher uma alternativa, mantenha simples: confirme que ela suporta o sistema de módulos que você usa hoje, é compatível com sua versão do Node, e não exige imports profundos.

Fixe uma versão compatível (com cautela)

Fixar versão pode ser o caminho mais rápido para estancar o problema, especialmente quando uma release mudou o formato de módulo. Trate isso como medida temporária. Você pode entregar, depois planejar a correção real (migrar para ESM ou trocar a dependência). Fique de olho em correções de segurança que você pode perder em versões antigas.

Use o ponto de entrada documentado, não um import profundo

Muitas quebras acontecem porque o código importa um caminho interno que costumava funcionar, tipo some-lib/dist/index.js. Após uma atualização, o pacote adiciona um mapa exports e bloqueia caminhos profundos. A correção costuma ser importar do entry público (ou de um subpath documentado exportado).

Se a dependência é ESM-only mas seu app é CJS

Você tem três escolhas realistas: migrar seu app para ESM, substituir a dependência, ou isolar ela.

Isolar costuma ser um bom compromisso: carregue o pacote ESM em um pequeno módulo wrapper (usando import() dinâmico), e mantenha o resto do código CommonJS enquanto planeja uma migração mais ampla.

Armadilhas comuns que fazem a quebra voltar

Muitas correções de ESM/CommonJS falham de novo porque o app não está realmente consistente. Funciona em um comando (geralmente dev), depois quebra em testes, CI ou produção porque um entry point ou toolchain diferente é usado.

Misturar require() e import no mesmo caminho de execução

O problema não é “usar os dois estilos em algum lugar do repo”. O problema é quando o mesmo caminho de execução pode rodar sob ambas as regras de módulo.

Exemplo: você adapta uma rota para usar import() dinâmico para uma dependência ESM-only, mas um script CLI ou teste ainda atinge o caminho antigo com require().

Se precisa misturar formatos por um tempo, mantenha a fronteira óbvia: um módulo wrapper que faz o import dinâmico, e o resto chama esse wrapper.

Enviar código TypeScript em vez de JS compilado

Isso aparece quando você faz deploy de uma pasta que ainda contém .ts (ou saída em estilo ESM) enquanto seu runtime espera CommonJS (ou o contrário). Localmente parece ok porque ts-node, um servidor de dev ou bundler compila para você.

Cheque o que realmente é deployado. Se seu servidor inicia com node dist/index.js, confirme que dist existe e contém o formato que você espera. Confirme também que os pontos de entrada (main, exports) apontam para arquivos gerados, não para a fonte.

Tooling de dev que altera o carregamento de módulos

Test runners, servidores de dev e transpilers podem mascarar problemas ao transformar imports em tempo de execução. A produção roda Node.js puro e atinge o mismatch cru.

Se o dev usa um runner customizado mas a produção usa node diretamente, trate “funciona em dev” como não comprovado até você rodar o comando de produção localmente.

Adicionar "type": "module" para consertar um arquivo e quebrar todo o resto

Definir "type": "module" muda o significado de todos os .js naquele pacote. Pode quebrar instantaneamente chamadas require(), arquivos de configuração que ferramentas esperam serem CommonJS e dependências antigas que assumem pontos de entrada CommonJS.

Se só precisa de ESM em uma área, considere usar .mjs para arquivos ESM e .cjs para CommonJS, ou isolar a mudança em um subpacote em vez de inverter o projeto inteiro.

Pacotes duais (comportamento diferente em ESM vs CJS)

Algumas bibliotecas publicam builds ESM e CJS. O Node pode escolher entradas diferentes dependendo se você import ou require, e dependendo das condições no exports. O problema é que ambas versões podem “funcionar” mas se comportarem ligeiramente diferente (forma do default export, efeitos colaterais).

Quando a quebra volta, fixe a versão da dependência e trave o ponto de entrada que você quer usar (usando o estilo de import documentado). Se a biblioteca continuar imprevisível, trocar por uma dependência mais simples costuma ser a solução mais rápida a longo prazo.

Exemplo: um protótipo que funciona em dev mas quebra em produção

Reconstrua do jeito certo
Se o codebase não vale a pena consertar, reconstruímos de forma limpa e preparamos para deploy.

Uma história comum com protótipos Node: tudo parece bem no laptop, depois os logs do deploy explodem. Localmente você rodou node server.js, clicou e a API respondeu. Em produção, o processo inicia, recebe a primeira requisição e trava.

Aqui vai um setup realista. O protótipo tem um arquivo de servidor CommonJS (server.js) que usa require() por toda parte. Uma dependência, adicionada por conveniência, é ESM-only.

O crash costuma parecer com isto:

Error [ERR_REQUIRE_ESM]: require() of ES Module ... not supported.
Instead change the require of ... to a dynamic import()

A causa raiz é direta: um arquivo CommonJS tenta carregar um pacote ESM-only com require(). O Node recusa porque ESM e CommonJS têm regras de carregamento diferentes.

Duas correções tendem a funcionar bem, dependendo de quanto você quer mudar.

Opção 1: mover um arquivo (ou a fronteira) para ESM

Se o arquivo do servidor é o único lugar que puxa a dependência ESM-only, mova essa fronteira para ESM.

Você pode renomear server.js para server.mjs e substituir require() por import, ou manter server.js e carregar a dependência ESM com um import dinâmico:

const esmLib = await import('esm-only-lib');

Isso mantém a maior parte do código como está e faz o pacote ESM carregar corretamente.

Opção 2: trocar a dependência por uma alternativa amigável ao CommonJS

Se converter um arquivo-chave para ESM desencadear muitas mudanças (testes, configs, outros imports), trocar pode ser mais rápido. Escolha uma dependência que suporte CommonJS (ou ofereça builds duais) e atualize o uso.

Para confirmar a correção, não apenas reinicie o servidor no mesmo ambiente. Faça um rebuild limpo e um cold start: exclua outputs de build e caches, reinstale dependências do zero, inicie o servidor fresco e acesse o endpoint que antes travava.

Próximos passos se você precisa de uma correção pronta para produção

Avance mais rápido tomando uma decisão e deixando-a consistente: mude o formato do app quando a maior parte do código e das ferramentas já pender nessa direção, mude a dependência quando um pacote é o caso isolado, ou ajuste o build quando a fonte está ok mas o output está errado.

Se for pedir ajuda a alguém, leve um snapshot limpo:

  • O texto exato do erro e a stack trace completa
  • Seu package.json (especialmente type, main, exports e dependências)
  • Sua versão do Node (e se é local, CI ou produção)
  • O arquivo e a linha que disparam a falha
  • O comando exato de execução (mais qualquer passo de build)

Se isso veio de um protótipo gerado por IA que foi remendado várias vezes, mismatches de módulo costumam aparecer junto com outros problemas de produção (auth quebrada, segredos expostos, estrutura difícil de manter). FixMyMess (fixmymess.ai) foca em tornar esses codebases herdados estáveis: diagnosticam o que está falhando, aplicam as menores mudanças seguras e verificam o resultado manualmente. Se quiser um ponto de partida de baixo risco, a auditoria gratuita deles pode rapidamente dizer qual fronteira de módulo está errada e qual caminho de correção é o mais seguro.

Perguntas Frequentes

Qual é a diferença mais simples entre ESM e CommonJS no Node?

CommonJS usa require() e module.exports, enquanto ESM usa import e export. Na prática, o problema é que o Node carrega um arquivo como apenas um dos formatos em tempo de execução; se o código ou uma dependência esperar o outro formato, vai travar.

Por que funciona em dev mas falha após o deploy?

Normalmente sua ferramenta de desenvolvimento está disfarçando a mistura. Um servidor de dev, um runner TypeScript ou um bundler pode reescrever imports ou agrupar dependências; em produção normalmente você executa node sobre arquivos em dist, o que revela o real mismatch de formato de módulo.

O que `Error [ERR_REQUIRE_ESM]` normalmente quer dizer?

Quase sempre significa que um arquivo CommonJS está chamando require() em um pacote que só distribui ESM. As correções rápidas são trocar esse import por um import() dinâmico, fixar/trocar a dependência por uma versão compatível com CJS, ou migrar essa parte do app para ESM.

Como corrijo “Cannot use import statement outside a module"?

O Node está tratando o arquivo como CommonJS, mas o arquivo contém sintaxe ESM. Verifique se falta "type": "module" no package.json, se você está usando .js quando deveria ser .mjs, ou se o build está emitindo ESM enquanto a execução inicia como CommonJS em produção.

Por que recebo “Named export … not found” ou comportamento estranho de export default?

Geralmente é uma incompatibilidade de interoperabilidade: você está importando exports nomeados de um módulo CommonJS. Tente importar o módulo todo como default e ler as propriedades dele, ou ajuste seu build/runtime para carregar a dependência no formato que ela espera.

Qual é a maneira mais rápida de encontrar a origem real do mismatch?

Comece pelo primeiro frame da stack trace que aponta para seu repositório (não node_modules). Observe a extensão do arquivo, a linha que faz import/require e o package.json mais próximo desse arquivo — isso normalmente mostra onde a decisão errada de formato de módulo ocorreu.

Como saber se uma dependência é ESM-only?

Olhe o package.json da dependência em node_modules e verifique exports, main e quaisquer condições de import/require. Se ela tiver exports e não fornecer um caminho para require, então require() vai falhar mesmo que versões antigas funcionassem.

Devo simplesmente adicionar ou remover `"type": "module"` no package.json?

Evite mudar "type": "module" como primeiro passo, pois isso altera o significado de todos os .js naquele pacote. Seja explícito: use .mjs para arquivos ESM e .cjs para CommonJS, ou isole a mudança em um subpacote em vez de inverter todo o projeto.

Quais configurações do TypeScript/build mais frequentemente causam quebra ESM/CJS?

Faça o output do build bater com a forma como você inicia o app. Se em produção você roda node dist/server.js, verifique se o TypeScript está emitindo o mesmo formato de módulo que o Node vai interpretar para aquele arquivo, e evite misturar imports entre src e dist em tempo de execução.

O que devo reunir antes de pedir ajuda, e a FixMyMess resolve isso rapidamente?

Envie o texto exato do erro e a stack trace completa, sua versão do Node (local e produção), o comando de start, e o package.json (especialmente type, main, exports e dependências). Se veio de um starter gerado por IA (Lovable, Bolt, v0, Cursor, Replit) e você precisa estabilizar rápido, FixMyMess (fixmymess.ai) pode rodar uma auditoria gratuita e aplicar a correção menor com segurança.