Módulos no Node.js: CommonJS vs ESModules
1. Introdução aos Sistemas de Módulos no Node.js
Quando o Node.js foi criado em 2009, o JavaScript ainda não possuía um sistema de módulos nativo. Para resolver problemas como escopo global poluído e dependências mal gerenciadas, a comunidade criou o CommonJS — um padrão que se tornou o coração do ecossistema Node.js por mais de uma década.
O CommonJS utiliza require() para importar módulos e module.exports para exportar funcionalidades. Esse sistema foi extremamente bem-sucedido, mas era específico para o ambiente server-side e não fazia parte da especificação oficial da linguagem.
Em 2015, com o ECMAScript 6 (ES2015), a linguagem JavaScript finalmente ganhou seu próprio sistema de módulos: os ESModules (ESM), com as palavras-chave import e export. O Node.js começou a suportar ESModules experimentalmente na versão 8.5 (2017) e, a partir da versão 12 (LTS), ofereceu suporte estável.
Hoje, ambos os sistemas coexistem no Node.js. Projetos modernos podem escolher entre CommonJS e ESModules, com boa interoperabilidade entre eles.
2. CommonJS: Sintaxe e Comportamento
O CommonJS carrega módulos de forma síncrona. Quando você usa require(), o Node.js lê, compila e executa o arquivo imediatamente, armazenando o resultado em cache para chamadas futuras.
Exemplo prático: criando um módulo utilitário
// utils.js
function formatDate(date) {
return date.toISOString().split('T')[0];
}
function capitalize(str) {
return str.charAt(0).toUpperCase() + str.slice(1);
}
const PI = 3.14159;
module.exports = {
formatDate,
capitalize,
PI
};
// app.js
const utils = require('./utils');
console.log(utils.formatDate(new Date())); // 2025-04-14
console.log(utils.capitalize('hello')); // Hello
console.log(utils.PI); // 3.14159
Você também pode usar exports como atalho para module.exports:
exports.formatDate = formatDate;
exports.capitalize = capitalize;
Atenção: exports é uma referência a module.exports. Se você reatribuir exports diretamente (exports = {}), perde a referência e quebra o módulo.
3. ESModules: Sintaxe e Comportamento
ESModules utilizam declarações estáticas — as importações e exportações são analisadas em tempo de compilação, antes da execução do código. Isso permite otimizações como tree-shaking.
Exemplo com export nomeado e default:
// math.mjs
export const sum = (a, b) => a + b;
export const subtract = (a, b) => a - b;
export default function multiply(a, b) {
return a * b;
}
// main.mjs
import multiply, { sum, subtract } from './math.mjs';
console.log(sum(5, 3)); // 8
console.log(subtract(10, 4)); // 6
console.log(multiply(2, 3)); // 6
Diferenças entre export nomeado e default:
- Export nomeado: permite múltiplas exportações por módulo. Na importação, os nomes devem corresponder (ou usar
aspara alias). - Export default: apenas um por módulo. Pode ser importado com qualquer nome.
// Export default com nome diferente na importação
import qualquerNome from './math.mjs'; // importa multiply
O carregamento assíncrono é nativo nos ESModules, o que permite carregar módulos sob demanda:
const module = await import('./dynamic.mjs');
4. Diferenças Técnicas Cruciais
Resolução de Caminhos e Extensões
No CommonJS, a extensão .js é opcional. No ESModules, você deve especificar a extensão completa:
// CommonJS (funciona)
const lib = require('./lib');
// ESModules (obrigatório)
import lib from './lib.js';
ESModules também exigem caminhos absolutos ou relativos — não aceitam módulos sem caminho (exceto para módulos core ou pacotes npm).
Strict Mode
ESModules executam automaticamente em strict mode. No CommonJS, você precisa declarar "use strict" explicitamente.
this no Escopo Global
// CommonJS
console.log(this === module.exports); // true
// ESModules
console.log(this); // undefined
__dirname e __filename
Essas variáveis não existem nos ESModules. Você precisa recriá-las:
import { fileURLToPath } from 'url';
import { dirname } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
5. Configuração e Interoperabilidade
Ativando ESModules no Projeto
Adicione "type": "module" ao package.json:
{
"name": "meu-projeto",
"type": "module",
"dependencies": {}
}
Com isso, todos os arquivos .js serão tratados como ESModules. Para usar CommonJS, use extensão .cjs. Para forçar ESM em um arquivo específico, use .mjs.
Importando CommonJS a partir de ESModules
// ESM importando CJS
import fs from 'fs'; // módulo core funciona
import pkg from 'lodash'; // pacote npm funciona
Importando ESModules a partir de CommonJS
Para importar um módulo ESM de dentro de um arquivo CommonJS, use createRequire:
// arquivo .cjs
const { createRequire } = require('module');
const require = createRequire(import.meta.url);
const meuModulo = require('./modulo-esm.mjs');
6. Implicações para o Ecossistema React
No ecossistema React, a maioria dos bundlers (Webpack, Vite, Rollup) suporta ambos os sistemas. No entanto, a escolha entre CommonJS e ESM impacta diretamente:
Importando Bibliotecas React
// CommonJS (tradicional)
const React = require('react');
const { useState } = require('react');
// ESModules (moderno)
import React, { useState } from 'react';
A maioria dos pacotes npm modernos fornece duas versões: uma em CommonJS (para Node.js) e outra em ESM (para bundlers). O campo exports no package.json define qual versão usar.
Boas Práticas para Projetos React/Node.js
- Projetos Node.js puros: prefira ESModules (a partir do Node.js 16+).
- Projetos React com Vite: use ESModules nativamente.
- Projetos React com Webpack: ambos funcionam, mas ESM permite tree-shaking mais eficiente.
- Bibliotecas publicadas no npm: forneça ambas as versões (CommonJS + ESM).
7. Performance e Boas Práticas
Tree-shaking
O maior benefício dos ESModules é o tree-shaking — eliminação de código morto durante o bundle. Como as importações são estáticas, bundlers como Webpack e Rollup podem analisar exatamente quais exportações são usadas:
// math.js
export const usado = () => 'usado';
export const naoUsado = () => 'nunca usado';
// app.js
import { usado } from './math.js';
// 'naoUsado' será removido no bundle final
Caching e Dependências Circulares
Ambos os sistemas fazem caching de módulos, mas lidam com dependências circulares de forma diferente:
- CommonJS: retorna o objeto
module.exportsparcialmente preenchido. - ESModules: lança erro se a dependência circular não for resolvida corretamente.
Recomendação para Novos Projetos
Use ESModules como padrão. É o futuro da linguagem, oferece melhor performance em bundlers, e o Node.js já oferece suporte completo. Apenas recorra ao CommonJS se precisar manter compatibilidade com pacotes legados ou ambientes que ainda não suportam ESM.
Referências
- Documentação oficial do Node.js: Módulos CommonJS — Guia completo sobre require, module.exports e comportamento do CommonJS no Node.js
- Documentação oficial do Node.js: ESModules — Especificação detalhada dos ESModules no Node.js, incluindo interoperabilidade
- MDN Web Docs: import — Referência completa da declaração import no JavaScript moderno
- Node.js: Migrando de CommonJS para ESModules — Guia oficial de interoperabilidade entre os dois sistemas de módulos
- Webpack: Tree Shaking — Explicação prática de como o tree-shaking funciona com ESModules no Webpack
- Vite: Por que Vite — Artigo que explica como o Vite utiliza ESModules nativos para desenvolvimento rápido
- npm: publicando pacotes com suporte a ESM e CJS — Documentação sobre o campo "exports" no package.json para publicar pacotes dual-module