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 as para 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.exports parcialmente 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