ESM vs CommonJS Modules
Entendendo os dois sistemas de módulos do Node.js - quando usar cada um, como migrar e como suportar ambos os consumidores a partir de um único pacote.
Busque em todas as páginas da documentação
Entendendo os dois sistemas de módulos do Node.js - quando usar cada um, como migrar e como suportar ambos os consumidores a partir de um único pacote.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
Cartão de receita de referência rápida - pronto para copiar e colar.
// package.json - declare este pacote como ESM
{
"name": "my-pkg",
"version": "1.0.0",
"type": "module"
}// ESM - import / export
import { readFile } from 'node:fs/promises';
export async function loadConfig(path) {
return JSON.parse(await readFile(path, 'utf8'));
}// CommonJS - require / module.exports
const { readFileSync } = require('node:fs');
module.exports.loadConfig = function (path) {
return JSON.parse(readFileSync(path, 'utf8'));
};// Importação dinâmica - funciona em AMBOS os sistemas
const mod = await import('some-esm-only-package');// Use createRequire para carregar CJS de ESM
import { createRequire } from 'node:module';
const require = createRequire(import.meta.url);
const legacy = require('some-cjs-only-package');Extensões explícitas:
.mjs - sempre ESM, independentemente do package.json..cjs - sempre CommonJS, independentemente do package.json..js - depende do campo "type" do package.json pai mais próximo.Quando usar isso: Sempre que você encontrar um erro "ERR_REQUIRE_ESM", um erro "Cannot use import statement outside a module", ou estiver decidindo como publicar um pacote.
Um script ESM que importa um pacote ESM moderno e um pacote CJS legado.
// package.json
{
"name": "mixed-modules",
"version": "1.0.0",
"type": "module",
"scripts": {
"start": "node src/index.js"
},
"dependencies": {
"chalk": "^5.3.0",
"lodash": "^4.17.21"
}
}// src/index.js - Arquivo de entrada ESM que mistura ambos os sistemas de módulos
import chalk from 'chalk'; // Pacote apenas ESM
import { createRequire } from 'node:module';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
import { readFile } from 'node:fs/promises';
// Reconstrói __dirname em ESM
const __dirname = dirname(fileURLToPath(import.meta.url));
// Carrega um pacote apenas CommonJS de um arquivo ESM
const require = createRequire(import.meta.url);
const _ = require('lodash');
// Carrega JSON em ESM (sintaxe de atributos de importação)
const pkg = JSON.parse(
await readFile(join(__dirname, '..', 'package.json'), 'utf8'),
);
console.log(chalk.green(`Executando ${pkg.name} v${pkg.version}`));
console.log(chalk.cyan('chunked:'), _.chunk([1, 2, 3, 4, 5], 2));O que isso demonstra:
chalk v5+)createRequire de node:module para carregar um pacote apenas CommonJS de ESMimport.meta.url + fileURLToPath para reconstruir __dirnamefs para evitar a sintaxe de atributos de importação ainda em evoluçãoawait de nível superior - legal apenas em ESMCommonJS foi o sistema de módulos original do Node.js. Cada arquivo .js é envolvido em uma função síncrona que recebe require, module, exports, __filename e __dirname. Como é síncrono, require('x') bloqueia até que x seja totalmente carregado.
ES Modules são o padrão JavaScript. Eles são assíncronos, estaticamente analisáveis e suportam recursos que CommonJS nunca poderia - await de nível superior, tree-shaking e live bindings. A contrapartida é que require não está disponível no escopo ESM, as extensões de arquivo são obrigatórias e o grafo de módulos é resolvido antes que qualquer código seja executado.
O Node.js escolhe um sistema de módulos por arquivo com base em:
.mjs é sempre ESM, .cjs é sempre CommonJS."type" do package.json pai mais próximo - "module" significa ESM, "commonjs" (ou ausente) significa CommonJS.De ESM você sempre pode carregar CommonJS com uma importação padrão ou createRequire. De CommonJS você não pode estaticamente require um módulo ESM - você deve usar import() dinâmico, que retorna uma promessa.
Convertendo um arquivo CJS para ESM:
// Antes - CommonJS
const path = require('node:path');
const { readFile } = require('node:fs/promises');
module.exports.read = (p) => readFile(path.resolve(p), 'utf8');
// Depois - ESM
import path from 'node:path';
import { readFile } from 'node:fs/promises';
export const read = (p) => readFile(path.resolve(p), 'utf8');Exportações de pacotes duplos - envie ambos ESM e CJS:
// package.json
{
"name": "dual-pkg",
"type": "module",
"main": "./dist/index.cjs",
"module": "./dist/index.js",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"require": "./dist/index.cjs"
}
}
}Importando JSON em ESM - use a sintaxe de atributos de importação (escapando os colchetes porque esta é prosa de markdown): import data from './data.json' with \{ type: 'json' \};. Está atrás de uma flag em versões mais antigas do Node, então muitos projetos ainda preferem readFile + JSON.parse.
Polyfill __dirname do ESM:
import { fileURLToPath } from 'node:url';
import { dirname } from 'node:path';
const __dirname = dirname(fileURLToPath(import.meta.url));Para TypeScript, "module": "NodeNext" é quase sempre a escolha certa para código Node.js. Ele:
"type" do package.json mais próximo..js em importações relativas (mesmo que a origem seja .ts).exports (import, require, types)..mts (força ESM) e .cts (força CJS)."module": "ES2022" compila para ESM, mas não impõe as regras de resolução do Node - use-o apenas se um bundler consumir a saída.
Sempre emparelhe "module": "NodeNext" com "moduleResolution": "NodeNext". Qualquer outro modo de resolução divergirá silenciosamente de como o Node realmente carrega os arquivos.
.js. import './foo' lança ERR_MODULE_NOT_FOUND. TypeScript com NodeNext impõe isso em tempo de compilação..js em um pacote "type": "module" é ESM; seu arquivo irmão .cjs é CommonJS. Isso funciona, mas a nomenclatura e as importações ficam confusas rapidamente - prefira um sistema por pacote.import data from './data.json' lança no Node moderno, a menos que você adicione with { type: 'json' } (escapado aqui porque markdown). Em caso de dúvida, JSON.parse(await readFile(...)).require não está disponível no escopo ESM. Tentar chamar require('x') dentro de um arquivo ESM lança ReferenceError: require is not defined. Use createRequire(import.meta.url) quando realmente precisar.__dirname e __filename são indefinidos em ESM. Reconstrua-os a partir de import.meta.url com fileURLToPath.require()-ing um pacote ESM. Você obtém ERR_REQUIRE_ESM. Migre para ESM ou use import() dinâmico, que é assíncrono e retorna uma promessa."type": "module" afeta todos os arquivos .js abaixo dele. Adicionar o campo inverte um projeto inteiro de uma vez. Renomeie qualquer coisa que deva permanecer CJS para .cjs.| Runtime / Opção | Pontos Fortes | Pontos Fracos |
|---|---|---|
| Node.js ESM | Padrão, à prova de futuro, await de nível superior | Extensões rigorosas, mais novo para algumas bibliotecas |
| Node.js CJS | Maduro, funciona com todas as bibliotecas | Sem await de nível superior, sem tree-shaking |
| Pacote duplo (ESM + CJS) | Suporta todos os consumidores | Configuração de build complexa, fácil de enviar tipos quebrados |
| Bun | Suporta ambos os sistemas de módulos nativamente, muito rápido | Runtime diferente, ecossistema ainda em maturação |
| Deno | Apenas ESM, seguro por padrão, TS nativo | Resolução de módulo diferente, importações baseadas em URL |
| Projeto legado apenas CJS | Custo zero de migração | Não pode usar bibliotecas ESM modernas estaticamente |
Nomeie-o como .mjs ou adicione "type": "module" ao package.json mais próximo. Caso contrário, o Node trata arquivos .js como CommonJS.
Não diretamente. require é um global apenas para CommonJS. Use createRequire(import.meta.url) de node:module se você precisar carregar um módulo CJS de ESM.
Apenas a forma dinâmica: const mod = await import('some-pkg'). Declarações import estáticas só funcionam em arquivos ESM.
.mjs é sempre ESM, .cjs é sempre CommonJS e .js depende do campo "type" do package.json mais próximo. Se "type" estiver ausente, .js é padrão para CommonJS.
Porque require() do CommonJS não pode carregar um pacote ESM sincronicamente. Converta seu arquivo para ESM ou use import() dinâmico que retorna uma promessa.
Quase sempre uma extensão de arquivo ausente. ESM requer o especificador completo - escreva ./foo.js, não ./foo. CommonJS é mais tolerante e adiciona a extensão para você.
Use "NodeNext" para qualquer código que o Node.js executará diretamente. Ele impõe as regras de resolução reais do Node, incluindo extensões .js obrigatórias e o campo exports. Use "ES2022" apenas quando um bundler (Vite, webpack, esbuild) processar a saída.
Porque NodeNext modela o especificador em tempo de execução. Em tempo de execução, o Node carrega foo.js - compilado de foo.ts, ou resolvido de volta para foo.ts por tsx/ts-node. Escrever ./foo.js é a única forma que funciona em ambos os casos.
Reconstrua-o a partir de import.meta.url:
import { fileURLToPath } from 'node:url';
import { dirname } from 'node:path';
const __dirname = dirname(fileURLToPath(import.meta.url));Use atributos de importação - import data from './data.json' with { type: 'json' } - ou leia e analise-o você mesmo com fs.readFile e JSON.parse. O último evita problemas de compatibilidade de flags entre versões do Node.
Não. O await de nível superior é um recurso apenas para ESM. Em CommonJS, você deve envolver o código assíncrono em uma função async e chamá-la.
É a maneira moderna de declarar os pontos de entrada públicos de um pacote. Ele suporta exportações condicionais - arquivos diferentes para import, require, types, browser, e assim por diante - e oculta arquivos que você não quer que os consumidores acessem.
Revisado por Chris St. John·Última atualização: 19 de jul. de 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥