Pular para o conteúdo
Cursos

DevClub

LógicaFront-endBack-endMobile

IA Club

IA na prática
Estudar programaçãoEstudar IA
LiçãoIniciantecódigo testado

ESM ou CommonJS no Node: import, require e o campo type

Quando usar import e quando usar require no Node, o que o type module muda no package.json e como sair dos erros de misturar os dois.

Rodolfo Mori12 min de leitura

O Node oferece dois sistemas de módulos: CommonJS, com require e module.exports, e ECMAScript Modules (ESM), com import e export. Os dois rodam no mesmo runtime, mas cada arquivo é classificado por sua extensão e pelo campo type do package.json.

Pense em duas normas de tomada no mesmo prédio. As duas fornecem energia, mas o plugue e as regras de ligação são diferentes. .cjs identifica CommonJS, .mjs identifica ESM e "type": "module" funciona como a placa que define o padrão para os arquivos .js daquele pacote. Misturar a sintaxe crua sem um adaptador produz erro antes de o módulo entregar seus exports.

No comportamento técnico, CommonJS carrega com require de forma síncrona e expõe module.exports; ESM liga imports e exports segundo o padrão da linguagem e analisa as dependências antes da execução do corpo.

Todos os exemplos abaixo são da API da livraria Página Viva: catálogo, estoque e frete. Cada saída foi copiada de uma execução real no Node 24.16.0 — e a versão importa bastante aqui, porque o Node 24 aceita coisas que o Node 16 recusava.

Dois sistemas de módulo dentro do mesmo Node

CommonJS nasceu junto com o Node, em 2009, quando o JavaScript ainda não tinha sistema de módulos nenhum. Era uma invenção do Node para resolver um problema que a linguagem não resolvia. Em 2015 a linguagem resolveu: import e export entraram na especificação e viraram o padrão do navegador — o mesmo que você viu em módulos import e export no JavaScript.

Sobrou para o Node a parte difícil: adotar o padrão novo sem quebrar o milhão de pacotes escritos no formato antigo. A convivência dos dois é o resultado disso. Começando pelo formato antigo, que ainda é o que você mais encontra:

js
const livros = [
  { isbn: '9788535914849', titulo: 'Grande Sertão: Veredas', preco: 89.9, qtd: 4 },
  { isbn: '9788573265484', titulo: 'O Cortiço', preco: 34.5, qtd: 0 },
  { isbn: '9788525056009', titulo: 'Torto Arado', preco: 59.9, qtd: 12 },
];

function disponivel(isbn) {
  const livro = livros.find((l) => l.isbn === isbn);
  return Boolean(livro && livro.qtd > 0);
}

module.exports = { livros, disponivel };
js
const { livros, disponivel } = require('./estoque.js');

console.log('livros no catálogo:', livros.length);
console.log('Torto Arado disponível?', disponivel('9788525056009'));
console.log('O Cortiço disponível? ', disponivel('9788573265484'));
livros no catálogo: 3 Torto Arado disponível? true O Cortiço disponível? false

require: síncrono, cacheado e resolvido em tempo de execução

require não é palavra reservada: é uma função que o Node injeta em todo arquivo CommonJS. Ela roda quando a linha dela roda, lê o arquivo do disco na hora e devolve o que estiver em module.exports naquele momento.

Como é uma função comum, o resultado dela é guardado em cache. O segundo require do mesmo arquivo não executa o arquivo de novo — devolve o mesmo objeto:

js
console.log('  [conexao.js] abrindo conexão com o banco');
module.exports = { aberta: true, desde: '2026-05-13' };
js
console.log('linha 1 do cache.js');
const a = require('./conexao.js');
const b = require('./conexao.js');

console.log('mesma instância?', a === b);
console.log('arquivos no cache:', Object.keys(require.cache).length);
linha 1 do cache.js [conexao.js] abrindo conexão com o banco mesma instância? true arquivos no cache: 2

Duas coisas ficam visíveis nessa saída. A primeira: linha 1 do cache.js foi impressa antes do log de dentro do conexao.js. O require só aconteceu quando o programa chegou nele. A segunda: a mensagem de abertura de conexão aparece uma vez só, e a === b é true. É esse cache que faz um pool de banco ou um cliente de API virar singleton sem você pedir.

Como o require é resolvido durante a execução, ele aceita ser condicional:

js
const destino = process.argv[2] ?? 'nacional';

const calcularFrete =
  destino === 'internacional'
    ? require('./frete-internacional.js')
    : require('./frete-nacional.js');

console.log(destino, '→ R$', calcularFrete(1.2).toFixed(2));
[frete-nacional.js] carregado nacional → R$ 15.06

Rodando com o argumento internacional, o outro arquivo é que carrega — e o primeiro nunca é lido:

[frete-internacional.js] carregado internacional → R$ 106.94

import: estático, analisado antes da primeira linha rodar

O import é outra coisa. Ele não é função, é declaração: o Node lê o arquivo inteiro, monta o grafo de dependências e executa todos os módulos importados antes de rodar a sua primeira instrução. Dá para ver isso invertendo a ordem de propósito, num projeto com "type": "module" no package.json:

js
console.log('  [estoque.js] carregado');

export const livros = [
  { isbn: '9788535914849', titulo: 'Grande Sertão: Veredas', preco: 89.9, qtd: 4 },
  { isbn: '9788573265484', titulo: 'O Cortiço', preco: 34.5, qtd: 0 },
  { isbn: '9788525056009', titulo: 'Torto Arado', preco: 59.9, qtd: 12 },
];

export function disponivel(isbn) {
  const livro = livros.find((l) => l.isbn === isbn);
  return Boolean(livro && livro.qtd > 0);
}
js
console.log('linha 1 do index.js');

import { livros, disponivel } from './estoque.js';

console.log('livros no catálogo:', livros.length);
console.log('O Cortiço disponível?', disponivel('9788573265484'));
[estoque.js] carregado linha 1 do index.js livros no catálogo: 3 O Cortiço disponível? false

O console.log do estoque saiu primeiro, mesmo estando abaixo. O import foi içado. Isso é ótimo para ferramenta — um empacotador consegue saber o que você usa sem executar nada —, mas cobra um preço: import não pode ficar dentro de um if.

js
const destino = process.argv[2] ?? 'nacional';

if (destino === 'internacional') {
  import calcularFrete from './frete-internacional.js';
}
file:///private/tmp/paginaviva-esm/envio.js:4 import calcularFrete from './frete-internacional.js'; ^^^^^^^^^^^^^

SyntaxError: Unexpected identifier ‘calcularFrete’ at compileSourceTextModule (node:internal/modules/esm/utils:318:16) at ModuleLoader.moduleStrategy (node:internal/modules/esm/translators:90:18) at #translate (node:internal/modules/esm/loader:435:20) Node.js v24.16.0

Quem carrega sob condição é o import() dinâmico, que é função, devolve promessa e pode aparecer em qualquer lugar:

js
const destino = process.argv[2] ?? 'nacional';

const modulo =
  destino === 'internacional'
    ? await import('./frete-internacional.js')
    : await import('./frete-nacional.js');

console.log(destino, '→ R$', modulo.default(1.2).toFixed(2));
[frete-nacional.js] carregado nacional → R$ 15.06

Repare no await solto no topo do arquivo, sem função em volta. Isso é top-level await, e só existe em ESM — guarde essa informação, porque ela volta mais para a frente com um erro no colo.

js
import { livros } from './estoque';

console.log(livros.length);
Error [ERR_MODULE_NOT_FOUND]: Cannot find module '/private/tmp/paginaviva-esm/estoque' imported from /private/tmp/paginaviva-esm/sem-extensao.js Did you mean to import "./estoque.js"? at finalizeResolution (node:internal/modules/esm/resolve:271:11) at moduleResolve (node:internal/modules/esm/resolve:865:10) Node.js v24.16.0

As outras causas dessa mesma família estão em Cannot find module no Node.

O campo type e as extensões .mjs e .cjs

Antes de executar qualquer arquivo, o Node decide de que tipo ele é. A regra é curta e vale a pena decorar:

1. Termina em .mjs? ES Module, sempre — o package.json não é consultado 2. Termina em .cjs? CommonJS, sempre — vale até dentro de um projeto ESM 3. Termina em .js? o Node procura o package.json mais próximo "type": "module" → ES Module "type": "commonjs" → CommonJS sem o campo type: tenta CommonJS e, se achar import ou export, reanalisa como ES Module e avisa no terminal

A linha 2 do diagrama é a mais útil na prática: a extensão ganha do package.json. Num projeto declarado como ESM, um arquivo .cjs continua sendo CommonJS e continua tendo require:

js
const { origem, calcular } = require('frete-correios');

console.log('extensão .cjs vence o "type": "module" do package.json');
console.log('resolveu para:', origem, '| PAC 1,2 kg: R$', calcular(1.2).toFixed(2));
extensão .cjs vence o "type": "module" do package.json resolveu para: cjs/index.cjs | PAC 1,2 kg: R$ 15.06

Essa é a porta de saída da migração: você troca o projeto inteiro para ESM e vai renomeando para .cjs os poucos arquivos que ainda não deu para converter.

O caso do package.json sem campo type

Aqui mora a mudança mais recente e a que mais confunde quem aprendeu com material de 2021. Num projeto sem type no package.json, um arquivo .js escrito com import funciona no Node 24 — o Node tenta ler como CommonJS, percebe a sintaxe de módulo e reanalisa o arquivo como ESM:

js
import { livros } from './estoque.js';

console.log('títulos:', livros.map((l) => l.titulo).join(', '));
(node:85958) [MODULE_TYPELESS_PACKAGE_JSON] Warning: Module type of file:///private/tmp/paginaviva/relatorio.js is not specified and it doesn't parse as CommonJS. Reparsing as ES module because module syntax was detected. This incurs a performance overhead. To eliminate this warning, add "type": "module" to /private/tmp/paginaviva/package.json. (Use `node --trace-warnings ...` to show where the warning was created) títulos: Grande Sertão: Veredas, O Cortiço, Torto Arado

Sobre o “performance overhead” que o aviso cita: medi as duas formas do mesmo arquivo, 30 execuções por rodada, 3 rodadas, num MacBook com Node 24.16.0. Com detecção de sintaxe deu 38,2 / 39,6 / 46,4 ms por execução; com o arquivo já declarado .mjs, 39,0 / 37,3 / 42,9 ms. Ou seja: num arquivo pequeno o custo some dentro do ruído da medição. O motivo de declarar o type não é velocidade — é você e o seu editor pararem de adivinhar o que aquele arquivo é.

Os dois erros de sintaxe que sobraram

O Cannot use import statement outside a module não sumiu: ele aparece quando o arquivo é declaradamente CommonJS. Basta o package.json dizer isso:

json
{
  "name": "paginaviva-legado",
  "version": "1.0.0",
  "type": "commonjs"
}
js
import { readFileSync } from 'node:fs';

console.log(readFileSync('./package.json', 'utf8').length, 'bytes lidos');
/private/tmp/paginaviva-cjs/relatorio.js:1 import { readFileSync } from 'node:fs'; ^^^^^^

SyntaxError: Cannot use import statement outside a module at wrapSafe (node:internal/modules/cjs/loader:1787:18) at Module._compile (node:internal/modules/cjs/loader:1828:20) at Object..js (node:internal/modules/cjs/loader:1985:10) Node.js v24.16.0

O irmão dele acontece no mesmo lugar, quando é o export que está do lado errado — num arquivo .cjs, por exemplo:

js
export const TAXA_CARTAO = 0.0399;
/private/tmp/paginaviva/exporta.cjs:1 export const TAXA_CARTAO = 0.0399; ^^^^^^

SyntaxError: Unexpected token ‘export’ at wrapSafe (node:internal/modules/cjs/loader:1787:18) at Module._compile (node:internal/modules/cjs/loader:1828:20) at Object..js (node:internal/modules/cjs/loader:1985:10) Node.js v24.16.0

Os dois querem dizer a mesma coisa: este arquivo é CommonJS e você escreveu sintaxe de ESM nele. A correção é sempre uma das três — trocar a sintaxe, renomear para .mjs ou pôr "type": "module" no package.json.

Misturando os dois: o que o Node 24 já aceita e o que ainda não

ESM importando CommonJS sempre funcionou. O caminho contrário é que era proibido — e essa é a parte que mudou. Hoje um arquivo CommonJS consegue dar require num módulo ESM, desde que ele seja síncrono:

js
export const TAXA_CARTAO = 0.0399;

export function comJuros(valor) {
  return valor * (1 + TAXA_CARTAO);
}
js
const precos = require('./precos.mjs');

console.log('o que veio:', Object.keys(precos));
console.log('Torto Arado no cartão: R$', precos.comJuros(59.9).toFixed(2));
o que veio: [ 'TAXA_CARTAO', 'comJuros' ] Torto Arado no cartão: R$ 62.29

Se você buscar esse assunto vai achar muito texto dizendo que isso lança ERR_REQUIRE_ESM. Foi verdade por anos, e deixou de ser a partir do Node 22.12. O que continua impossível é dar require num módulo que usa top-level await, porque aí não existe resultado síncrono para devolver:

js
const dolar = await Promise.resolve(5.42);

export const cotacaoDolar = dolar;
export const emReais = (usd) => usd * dolar;
js
const { emReais } = require('./cotacao.mjs');

console.log('livro importado: R$', emReais(19.9).toFixed(2));
Error [ERR_REQUIRE_ASYNC_MODULE]: require() cannot be used on an ESM graph with top-level await. Use import() instead. To see where the top-level await comes from, use --experimental-print-required-tla. From /private/tmp/paginaviva/importado.js Requiring /private/tmp/paginaviva/cotacao.mjs at ModuleJobSync.runSync (node:internal/modules/esm/module_job:539:13) at ModuleLoader.importSyncForRequire (node:internal/modules/esm/loader:350:47) at loadESMFromCJS (node:internal/modules/cjs/loader:1673:24) Node.js v24.16.0

A mensagem já entrega a correção: import() dinâmico, que devolve promessa e por isso pode esperar o await lá de dentro terminar.

js
async function main() {
  const { emReais } = await import('./cotacao.mjs');
  console.log('livro importado: R$', emReais(19.9).toFixed(2));
}

main();
livro importado: R$ 107.86

Falta o atrito na direção mais comum: ESM importando um arquivo CommonJS. Isso funciona, mas os nomes nem sempre chegam. O Node lê o texto do arquivo CommonJS procurando atribuições a exports para oferecer imports nomeados — e quando o arquivo monta as exportações em tempo de execução, não tem o que detectar:

js
const REGRAS = { PRIMEIRACOMPRA: 10, LEIAMAIS: 25 };

for (const codigo of Object.keys(REGRAS)) {
  module.exports[codigo.toLowerCase()] = (valor) =>
    valor - valor * (REGRAS[codigo] / 100);
}
js
import { leiamais } from './cupons.cjs';

console.log('R$', leiamais(59.9).toFixed(2));
file:///private/tmp/paginaviva-esm/checkout.js:1 import { leiamais } from './cupons.cjs'; ^^^^^^^^ SyntaxError: Named export 'leiamais' not found. The requested module './cupons.cjs' is a CommonJS module, which may not support all module.exports as named exports. CommonJS modules can always be imported via the default export, for example using:

import pkg from ‘./cupons.cjs’; const { leiamais } = pkg;

plaintext
at #asyncInstantiate (node:internal/modules/esm/module_job:327:21)

Node.js v24.16.0

O próprio erro escreve a correção: importe o default e desestruture depois. Aí o module.exports inteiro chega como um objeto:

js
import cupons from './cupons.cjs';

console.log('exports detectados:', Object.keys(cupons));
console.log('Torto Arado com cupom LEIAMAIS: R$', cupons.leiamais(59.9).toFixed(2));
exports detectados: [ 'primeiracompra', 'leiamais' ] Torto Arado com cupom LEIAMAIS: R$ 44.92

É por isso que import express from 'express' funciona: o Express é CommonJS, e o que você recebe no express é o module.exports dele.

__dirname, __filename e require não existem em ESM

Aquelas variáveis que todo tutorial de CommonJS usa não são do JavaScript: são do embrulho que o Node coloca em volta de arquivo CommonJS. Em ESM, o embrulho não existe:

js
console.log('__dirname:', __dirname);
file:///private/tmp/paginaviva-esm/caminho.js:1 console.log('__dirname:', __dirname); ^

ReferenceError: __dirname is not defined in ES module scope This file is being treated as an ES module because it has a ‘.js’ file extension and ‘/private/tmp/paginaviva-esm/package.json’ contains “type”: “module”. To treat it as a CommonJS script, rename it to use the ‘.cjs’ file extension. at file:///private/tmp/paginaviva-esm/caminho.js:1:27 Node.js v24.16.0

O mesmo acontece com require dentro de um arquivo ESM: ReferenceError: require is not defined in ES module scope, you can use import instead. Repare que a mensagem inteira, nos dois casos, explica por que o arquivo virou ESM e o que renomear para desfazer. É o erro mais bem escrito do Node.

A substituição direta é import.meta, que carrega o caminho do próprio módulo:

js
import { readFileSync } from 'node:fs';
import { join } from 'node:path';

console.log('import.meta.dirname :', import.meta.dirname);
console.log('import.meta.filename:', import.meta.filename);

const pkg = readFileSync(join(import.meta.dirname, 'package.json'), 'utf8');
console.log('name do package.json:', JSON.parse(pkg).name);
import.meta.dirname : /private/tmp/paginaviva-esm import.meta.filename: /private/tmp/paginaviva-esm/caminho-ok.js name do package.json: paginaviva-esm

Com isso na mão, ler e escrever arquivo no Node segue igual ao que você já conhece. E quando um pacote antigo só funciona com require, dá para fabricar um require local:

js
import { createRequire } from 'node:module';

const require = createRequire(import.meta.url);
const pkg = require('./package.json');

console.log('versão da lib:', pkg.version, '| tipo:', pkg.type);
versão da lib: 1.0.0 | tipo: module

Os campos main e exports: como a sua lib é encontrada

Quando você escreve require('frete-correios') ou import ... from 'frete-correios', o Node vai até o package.json do pacote para descobrir qual arquivo abrir. O campo antigo é o main, um caminho só. O campo moderno é o exports, que responde diferente para cada sistema de módulo:

json
{
  "name": "frete-correios",
  "version": "2.0.0",
  "exports": {
    ".": {
      "import": "./esm/index.mjs",
      "require": "./cjs/index.cjs"
    },
    "./tabela": "./esm/tabela.mjs"
  }
}

O mesmo nome de pacote, pedido dos dois lados, entrega dois arquivos:

js
const frete = require('frete-correios');
console.log('CommonJS recebeu:', frete.origem, '| R$', frete.calcular(1.2).toFixed(2));
CommonJS recebeu: cjs/index.cjs | R$ 15.06
js
import { origem, calcular } from 'frete-correios';
console.log('ESM recebeu:      ', origem, '| R$', calcular(1.2).toFixed(2));
ESM recebeu: esm/index.mjs | R$ 15.06

É assim que uma biblioteca publica os dois formatos sem quebrar ninguém. O exports tem um segundo efeito, que costuma pegar de surpresa: ele é uma lista fechada. Caminho que não está declarado ali deixa de ser importável, mesmo existindo no disco:

js
import { margem } from 'frete-correios/esm/segredo.mjs';
console.log(margem);
Error [ERR_PACKAGE_PATH_NOT_EXPORTED]: Package subpath './esm/segredo.mjs' is not defined by "exports" in /private/tmp/paginaviva-esm/node_modules/frete-correios/package.json imported from /private/tmp/paginaviva-esm/espia.js at exportsNotFound (node:internal/modules/esm/resolve:314:10) at packageExportsResolve (node:internal/modules/esm/resolve:662:9) at packageResolve (node:internal/modules/esm/resolve:774:12) Node.js v24.16.0

Com main, qualquer arquivo interno do pacote era importável e virava API acidental. Com exports, a pessoa que mantém a biblioteca escolhe o que é público — e é por isso que aquele import “que sempre funcionou” para de funcionar depois de um npm update.

O que escolher em projeto novo, e como migrar um antigo

Em projeto novo, escreva "type": "module" no package.json na primeira hora e não pense mais no assunto. É o padrão da linguagem, é o que roda no navegador, é o que o TypeScript e os empacotadores esperam. A tabela abaixo é a decisão que eu uso hoje:

situação escolha por quê
API nova em Node 24 ESM, "type": "module" padrão da linguagem, top-level await de graça
projeto legado grande, em produção fica em CommonJS migrar sem necessidade só produz risco
pacote que você publica no npm ESM com exports dual quem consome escolhe o lado
script solto de uma vez só .mjs, sem package.json roda em ESM sem configurar nada
arquivo que precisa de require no meio de um projeto ESM renomeie para .cjs a extensão ganha do type

Para migrar um projeto existente, a ordem que dá menos trabalho é esta. Primeiro, declare o tipo:

diff
 {
   "name": "paginaviva",
   "version": "1.0.0",
+  "type": "module",
   "main": "index.js"
 }

Depois rode o projeto e siga os erros, que são poucos e sempre os mesmos: troque require(...) por import ... from '...', acrescente a extensão nos caminhos relativos, troque module.exports = x por export default x, e troque __dirname por import.meta.dirname. O que resistir — geralmente um script antigo ou uma dependência sem versão ESM — você renomeia para .cjs e resolve depois. Um arquivo por vez, com o servidor subindo entre cada passo.

O próximo passo da trilha de Node é usar esse conhecimento para valer: ler e escrever arquivos, ler variáveis de ambiente e subir o primeiro servidor HTTP. Se ainda faltar o começo, o guia de Node mostra a ordem inteira, e o que é o Node.js explica de onde vem esse runtime que teve que abrigar dois sistemas de módulo ao mesmo tempo.

Monte dois diretórios mínimos: um com "type": "module" e outro sem o campo. Exporte a mesma função com export no primeiro e module.exports no segundo, importe e rode ambos. Em seguida troque apenas um arquivo para .cjs ou .mjs e preveja qual regra vencerá. O exercício está completo quando você explica o tipo de cada arquivo sem olhar para a sintaxe dentro dele.

  • node
  • esm
  • commonjs
  • modulos
  • import
  • require

Perguntas frequentes

Posso usar import e require no mesmo arquivo?
Não com as duas sintaxes cruas. Cada arquivo é de um tipo só. O que dá para fazer é usar import() dinâmico dentro de um arquivo CommonJS, e createRequire dentro de um arquivo ESM — as duas saídas de emergência existem justamente para conviver com dependência do outro lado.
O TypeScript muda essa decisão?
Muda o que você escreve, não o que o Node executa. Você escreve import no .ts, e o compilador emite require ou import conforme a opção module do tsconfig. O arquivo .js que sai continua sendo classificado pela extensão e pelo campo type do package.json, igualzinho.
Todo pacote do npm já publica em ESM?
Não. Boa parte do ecossistema ainda é CommonJS puro, e muita biblioteca publica os dois formatos com o campo exports. Como o Node 24 consegue dar require em um módulo ESM síncrono, o atrito caiu bastante — mas continua valendo ler o package.json do pacote antes de brigar com o import.
E no navegador, muda alguma coisa?
O navegador só entende ES Modules, com script type="module". CommonJS nunca rodou nativamente ali: quando você vê require em código de front-end é porque um empacotador traduziu antes. É por isso que o padrão da linguagem venceu no longo prazo.

Dúvidas e comentários

Travou em algum passo? Pergunte aqui — a equipe e outros alunos respondem.

Todo o código deste artigo foi executado em Node 24.16.0, e as saídas exibidas são as reais — como produzimos este conteúdo.

Fontes consultadas

  1. Node.js — Modules: ECMAScript modules — nodejs.org
  2. Node.js — Modules: Packages — nodejs.org
  3. Node.js — Modules: CommonJS modules — nodejs.org

Continue por aqui