17  Módulos e CommonJS vs. ES Modules

O objetivo é que você compreenda as características, vantagens e desvantagens de cada sistema de módulos, além de dominar a criação e importação de módulos personalizados. O material é estruturado em duas partes: uma teoria detalhada sobre CommonJS e ES Modules, seguida de um exemplo prático que demonstra a implementação em ambos os formatos.


17.1 Teoria: Diferenças entre CommonJS e ES Modules

17.1.1 Introdução aos Módulos no Node.js

Módulos são blocos de código reutilizáveis que permitem organizar aplicações Node.js de forma modular. Eles encapsulam funcionalidades específicas, como funções, objetos ou classes, e podem ser importados em outros arquivos para evitar repetição de código e melhorar a manutenibilidade. No Node.js, os módulos são essenciais para estruturar projetos escaláveis, desde pequenos scripts até APIs complexas.

O Node.js suporta dois sistemas de módulos principais: - CommonJS: O sistema original do Node.js, introduzido em 2009. - ES Modules (ESM): O padrão oficial do JavaScript (ECMAScript), introduzido no ES6 (2015) e totalmente suportado no Node.js a partir da versão 12.

Ambos os sistemas têm sintaxes e comportamentos distintos, e entender suas diferenças é crucial para escolher o mais adequado ao seu projeto e escrever código compatível com as práticas modernas.

17.1.2 CommonJS: O Sistema de Módulos Original do Node.js

CommonJS é o sistema de módulos nativo do Node.js desde sua criação. Ele foi projetado para permitir modularidade em ambientes JavaScript fora do navegador, onde o ES6 ainda não existia. CommonJS é amplamente usado em projetos legados e em muitos pacotes npm, embora esteja sendo gradualmente substituído pelos ES Modules em projetos modernos.

17.1.2.1 Características do CommonJS

  • Sintaxe: Usa require() para importar módulos e module.exports ou exports para exportá-los.
  • Carregamento Síncrono: Módulos CommonJS são carregados de forma síncrona, ou seja, o Node.js lê e executa o módulo no momento da importação.
  • Escopo do Módulo: Cada arquivo é tratado como um módulo independente, com seu próprio escopo. Variáveis locais não são acessíveis fora do módulo, a menos que sejam explicitamente exportadas.
  • Cópia de Valores: Quando um módulo é importado, o Node.js cria uma cópia dos valores exportados. Alterações no módulo original após a importação não afetam a cópia importada (exceto para objetos, que são passados por referência).
  • Suporte Nativo: CommonJS é suportado em todas as versões do Node.js, sem necessidade de configuração adicional.

17.1.2.2 Sintaxe do CommonJS

  • Exportação:

    // arquivo: math.js
    function soma(a, b) {
      return a + b;
    }
    module.exports = { soma };

    Ou, alternativamente:

    exports.soma = function (a, b) {
      return a + b;
    };
  • Importação:

    // arquivo: index.js
    const math = require('./math');
    console.log(math.soma(2, 3)); // 5
  • Notas:

    • O caminho ./math indica um módulo local no mesmo diretório. O sufixo .js é opcional.
    • require() pode importar módulos nativos (ex.: fs, http) ou pacotes npm (ex.: lodash).

17.1.2.3 Vantagens do CommonJS

  • Simplicidade: A sintaxe é direta e fácil de entender para iniciantes.
  • Compatibilidade: Funciona em todas as versões do Node.js e é amplamente usado em pacotes npm legados.
  • Carregamento Dinâmico: Permite importar módulos condicionalmente ou dinamicamente (ex.: require(algumaVariavel)).
  • Suporte em Ferramentas: Muitas ferramentas de build e bundlers (ex.: Webpack) suportam CommonJS nativamente.

17.1.2.4 Desvantagens do CommonJS

  • Carregamento Síncrono: Pode causar atrasos em aplicações grandes, especialmente ao carregar muitos módulos no início.
  • Falta de Tree Shaking: Não suporta tree shaking (remoção de código morto), o que pode aumentar o tamanho do bundle em aplicações frontend.
  • Incompatibilidade com ES Modules: Não pode ser usado diretamente com ES Modules sem ferramentas de conversão (ex.: Babel).
  • Sintaxe Menos Moderna: Comparada aos ES Modules, a sintaxe é considerada menos elegante e menos alinhada com o JavaScript moderno.

17.1.2.5 Como o CommonJS Funciona Internamente

Quando você usa require('./math'), o Node.js segue este processo: 1. Resolução do Módulo: O Node.js localiza o arquivo ou pacote com base no caminho fornecido. 2. Carregamento: O arquivo é lido e executado. 3. Wrapping: O Node.js envolve o código do módulo em uma função para criar um escopo isolado: javascript (function (exports, require, module, __filename, __dirname) { // Código do módulo }); 4. Caching: O módulo é armazenado em cache, garantindo que múltiplas chamadas a require() retornem a mesma instância. 5. Exportação: O objeto module.exports é retornado para o código que chamou require().

17.1.3 ES Modules: O Padrão Moderno do JavaScript

ES Modules (ESM) é o sistema de módulos oficial do ECMAScript, introduzido no ES6 (2015). Ele foi projetado para ser um padrão unificado para JavaScript, funcionando tanto no navegador quanto no servidor (Node.js). Desde a versão 12 do Node.js (2019), os ES Modules são suportados nativamente, e em 2025, eles são o padrão recomendado para novos projetos devido à sua integração com o ecossistema JavaScript moderno.

17.1.3.1 Características dos ES Modules

  • Sintaxe: Usa import para importar e export para exportar.
  • Carregamento Assíncrono: Módulos ESM são carregados de forma assíncrona, permitindo melhor performance em aplicações modernas.
  • Escopo Estrito: Funcionam automaticamente em modo estrito ("use strict"), garantindo maior segurança.
  • Suporte a Tree Shaking: Permite que ferramentas de build (ex.: Rollup, Vite) removam código não utilizado, otimizando o tamanho do bundle.
  • Interoperabilidade: Compatível com navegadores e ferramentas modernas, facilitando o desenvolvimento full-stack.

17.1.3.2 Sintaxe dos ES Modules

  • Exportação:

    // arquivo: math.mjs
    export function soma(a, b) {
      return a + b;
    }

    Ou, exportando múltiplos itens:

    export const soma = (a, b) => a + b;
    export const subtracao = (a, b) => a - b;
  • Importação:

    // arquivo: index.mjs
    import { soma } from './math.mjs';
    console.log(soma(2, 3)); // 5
  • Exportação Padrão (Default):

    // arquivo: math.mjs
    export default function soma(a, b) {
      return a + b;
    }
    // arquivo: index.mjs
    import soma from './math.mjs';
    console.log(soma(2, 3)); // 5
  • Notas:

    • Arquivos ESM usam a extensão .mjs ou exigem "type": "module" no package.json.
    • O caminho deve incluir a extensão .mjs (ou .js com configuração apropriada).

17.1.3.3 Vantagens dos ES Modules

  • Padrão Oficial: Alinhado com o ECMAScript, garantindo compatibilidade com navegadores e Node.js.
  • Carregamento Assíncrono: Melhora a performance ao carregar módulos sob demanda.
  • Tree Shaking: Reduz o tamanho do código em aplicações frontend.
  • Sintaxe Moderna: Mais clara e consistente com o JavaScript moderno (ex.: import é mais intuitivo que require).
  • Suporte a Top-Level Await: Permite usar await diretamente no nível superior do módulo (ex.: const data = await fetch(...)).

17.1.3.4 Desvantagens dos ES Modules

  • Configuração Adicional: Em projetos Node.js, é necessário configurar "type": "module" ou usar a extensão .mjs.
  • Compatibilidade com CommonJS: Nem todos os pacotes npm são compatíveis com ESM, exigindo soluções como import() dinâmico ou ferramentas de conversão.
  • Curva de Aprendizado: A sintaxe e as regras (ex.: necessidade de extensões nos caminhos) podem confundir iniciantes.
  • Suporte Parcial em Versões Antigas: Embora irrelevante em 2025, projetos legados em versões antigas do Node.js (pré-v12) não suportam ESM nativamente.

17.1.3.5 Como os ES Modules Funcionam Internamente

Quando você usa import, o Node.js segue este processo: 1. Resolução do Módulo: O Node.js localiza o arquivo com base no caminho, exigindo a extensão (ex.: .mjs). 2. Parsing: O módulo é analisado para identificar todas as dependências (import e export). 3. Carregamento Assíncrono: As dependências são carregadas em paralelo, usando promises. 4. Linkagem: Os exports são vinculados aos imports, criando referências ao vivo (live bindings). 5. Execução: O código do módulo é executado, e os valores exportados ficam disponíveis.

Live Bindings: Diferentemente do CommonJS, os ES Modules usam vinculação ao vivo. Se o valor exportado mudar no módulo original, a mudança é refletida no módulo que importou:

// arquivo: counter.mjs
export let count = 0;
export function increment() {
  count++;
}
// arquivo: index.mjs
import { count, increment } from './counter.mjs';
console.log(count); // 0
increment();
console.log(count); // 1

17.1.4 Diferenças Chave entre CommonJS e ES Modules

Característica CommonJS ES Modules
Sintaxe require / module.exports import / export
Carregamento Síncrono Assíncrono
Extensão do Arquivo .js (padrão) .mjs ou .js com "type": "module"
Escopo Escopo isolado, mas não estrito Modo estrito por padrão
Tree Shaking Não suportado Suportado
Live Bindings Cópia de valores Vinculação ao vivo
Top-Level Await Não suportado Suportado
Compatibilidade Ampla (todos os Node.js, pacotes npm) Nativo desde Node.js 12, mas requer configuração
Uso Dinâmico require(variavel) import() (importação dinâmica)

17.1.4.1 Exemplo de Diferenças Práticas

CommonJS:

// math.js
module.exports = {
  soma: (a, b) => a + b
};
// index.js
const { soma } = require('./math');
console.log(soma(2, 3)); // 5

ES Modules:

// math.mjs
export const soma = (a, b) => a + b;
// index.mjs
import { soma } from './math.mjs';
console.log(soma(2, 3)); // 5

17.1.4.2 Configuração no Node.js

Para usar ES Modules no Node.js, você deve: - Usar a extensão .mjs para arquivos ESM. - Ou adicionar "type": "module" ao package.json: json { "name": "meu-projeto", "type": "module" } - Para CommonJS, o padrão é "type": "commonjs" (ou ausência do campo).

17.1.4.3 Interoperabilidade

Misturar CommonJS e ES Modules pode ser desafiador: - Importar CommonJS em ESM: javascript import { createRequire } from 'module'; const require = createRequire(import.meta.url); const modulo = require('./modulo.cjs'); Ou usar importação dinâmica: javascript const modulo = await import('./modulo.cjs');

  • Importar ESM em CommonJS:

    const { soma } = await import('./math.mjs');

Para projetos grandes, ferramentas como Babel ou esbuild podem converter entre formatos.

17.1.5 Quando Usar CommonJS vs. ES Modules?

  • Use CommonJS:
    • Em projetos legados ou pacotes npm que não suportam ESM.
    • Quando a simplicidade e a compatibilidade são prioridades.
    • Em scripts rápidos onde a configuração extra do ESM não é necessária.
  • Use ES Modules:
    • Em projetos novos ou modernos (recomendado em 2025).
    • Quando você precisa de tree shaking ou integração com navegadores.
    • Para aproveitar recursos como top-level await e sintaxe moderna.
    • Em projetos full-stack onde a consistência entre frontend e backend é importante.

Em 2025, ES Modules é o padrão recomendado para novos projetos Node.js devido à sua integração com o ecossistema JavaScript moderno e suporte a ferramentas como Vite, Rollup e TypeScript.


17.2 Exemplo Prático: Criar e Importar um Módulo Personalizado

Neste exemplo prático, vamos criar um módulo personalizado para realizar operações matemáticas (soma, subtração e multiplicação) e importá-lo em um arquivo principal. Implementaremos o exemplo em CommonJS e ES Modules para demonstrar as diferenças na prática.

17.2.1 Objetivo do Exemplo

  • Criar um módulo personalizado chamado math que exporta funções matemáticas.
  • Importar o módulo em um arquivo principal (index.js ou index.mjs) e usar suas funções.
  • Testar ambos os formatos (CommonJS e ES Modules) em um projeto Node.js.

17.2.2 Passo 1: Configurar o Projeto

  1. Crie uma nova pasta para o projeto:

    mkdir modulo-personalizado
    cd modulo-personalizado
    npm init -y
  2. O package.json será criado com configurações padrão:

    {
      "name": "modulo-personalizado",
      "version": "1.0.0",
      "description": "",
      "main": "index.js",
      "scripts": {
        "test": "echo \"Error: no test specified\" && exit 1"
      },
      "keywords": [],
      "author": "",
      "license": "ISC"
    }
  3. Para ES Modules, modifique o package.json (opcional, usado na segunda parte):

    {
      "name": "modulo-personalizado",
      "version": "1.0.0",
      "type": "module",
      "main": "index.js",
      "scripts": {
        "test": "echo \"Error: no test specified\" && exit 1"
      },
      "keywords": [],
      "author": "",
      "license": "ISC"
    }
  4. Instale o nodemon para reiniciar o servidor automaticamente (opcional):

    npm install --save-dev nodemon

    Adicione ao package.json:

    "scripts": {
      "start": "node index.js",
      "dev": "nodemon index.js"
    }

17.2.3 Passo 2: Implementação com CommonJS

  1. Criar o Módulo Personalizado: Crie um arquivo math.js:

    // math.js
    function soma(a, b) {
      return a + b;
    }
    
    function subtracao(a, b) {
      return a - b;
    }
    
    function multiplicacao(a, b) {
      return a * b;
    }
    
    module.exports = {
      soma,
      subtracao,
      multiplicacao
    };
  2. Importar e Usar o Módulo: Crie um arquivo index.js:

    // index.js
    const math = require('./math');
    
    console.log('Soma: ', math.soma(5, 3)); // Soma: 8
    console.log('Subtração: ', math.subtracao(5, 3)); // Subtração: 2
    console.log('Multiplicação: ', math.multiplicacao(5, 3)); // Multiplicação: 15
  3. Executar o Projeto:

    node index.js

    Ou, com nodemon:

    npm run dev

    Saída esperada:

    Soma: 8
    Subtração: 2
    Multiplicação: 15
  4. Explicação:

    • O módulo math.js exporta um objeto contendo três funções usando module.exports.
    • O index.js importa o módulo com require e acessa as funções via math.soma, math.subtracao, etc.
    • O carregamento é síncrono, e o Node.js armazena o módulo em cache após a primeira importação.

17.2.4 Passo 3: Implementação com ES Modules

  1. Atualizar o package.json: Certifique-se de que o package.json inclui:

    "type": "module"
  2. Criar o Módulo Personalizado: Crie um arquivo math.mjs:

    // math.mjs
    export function soma(a, b) {
      return a + b;
    }
    
    export function subtracao(a, b) {
      return a - b;
    }
    
    export function multiplicacao(a, b) {
      return a * b;
    }
    
    // Exportação padrão (opcional)
    export default {
      soma,
      subtracao,
      multiplicacao
    };
  3. Importar e Usar o Módulo: Crie um arquivo index.mjs:

    // index.mjs
    import { soma, subtracao, multiplicacao } from './math.mjs';
    // Ou, usando importação padrão:
    // import math from './math.mjs';
    
    console.log('Soma: ', soma(5, 3)); // Soma: 8
    console.log('Subtração: ', subtracao(5, 3)); // Subtração: 2
    console.log('Multiplicação: ', multiplicacao(5, 3)); // Multiplicação: 15
  4. Executar o Projeto:

    node index.mjs

    Ou, com nodemon:

    npm run dev

    Saída esperada:

    Soma: 8
    Subtração: 2
    Multiplicação: 15
  5. Explicação:

    • O módulo math.mjs usa export para exportar funções individualmente e, opcionalmente, um objeto padrão com export default.
    • O index.mjs importa as funções com import, especificando o caminho com a extensão .mjs.
    • O carregamento é assíncrono, e os ES Modules suportam live bindings, permitindo que alterações no módulo original sejam refletidas.

17.2.5 Passo 4: Testando Diferenças com Live Bindings

Para demonstrar os live bindings dos ES Modules, vamos criar um exemplo que mostra a diferença em relação ao CommonJS.

  1. CommonJS (Cópia de Valores):

    // counter.js
    let count = 0;
    
    function increment() {
      count++;
      console.log('Count no módulo:', count);
    }
    
    module.exports = { count, increment };
    // index.js
    const { count, increment } = require('./counter');
    console.log('Count inicial:', count); // 0
    increment();
    console.log('Count após increment:', count); // 0 (não reflete a mudança)
  2. ES Modules (Live Bindings):

    // counter.mjs
    export let count = 0;
    
    export function increment() {
      count++;
      console.log('Count no módulo:', count);
    }
    // index.mjs
    import { count, increment } from './counter.mjs';
    console.log('Count inicial:', count); // 0
    increment();
    console.log('Count após increment:', count); // 1 (reflete a mudança)
  3. Executar:

    • Para CommonJS: node index.js
    • Para ES Modules: node index.mjs

    Saída do CommonJS:

    Count inicial: 0
    Count no módulo: 1
    Count após increment: 0

    Saída do ES Modules:

    Count inicial: 0
    Count no módulo: 1
    Count após increment: 1
  4. Explicação:

    • No CommonJS, count é uma cópia do valor exportado, então mudanças no módulo original não afetam o valor importado.
    • Nos ES Modules, count é uma referência ao vivo, refletindo mudanças no módulo original.

17.2.6 Passo 5: Boas Práticas

  1. Organize Módulos:
    • Crie uma pasta lib ou utils para módulos reutilizáveis.
    • Nomeie arquivos de forma descritiva (ex.: mathOperations.js).
  2. Use ES Modules em Projetos Novos:
    • Configure "type": "module" no package.json para consistência.
    • Prefira .js com ESM em vez de .mjs para evitar extensões específicas.
  3. Evite Misturar Formatos:
    • Escolha um sistema (CommonJS ou ESM) para todo o projeto para evitar problemas de interoperabilidade.
    • Use import() dinâmico para casos excepcionais.
  4. Versionamento:
    • Adicione os arquivos ao Git:

      git init
      git add .
      git commit -m "Módulo personalizado com CommonJS e ES Modules"
  5. Depuração:
    • Use o VS Code para depurar:
      • Adicione breakpoints nos arquivos index.js ou index.mjs.

      • Configure o launch.json para ESM, se necessário:

        {
          "version": "0.2.0",
          "configurations": [
            {
              "type": "node",
              "request": "launch",
              "name": "Launch ESM",
              "program": "${workspaceFolder}/index.mjs"
            }
          ]
        }

17.3 Conclusão

Neste capítulo, você aprendeu: - CommonJS: Sistema de módulos síncrono, usando require e module.exports, ideal para projetos legados. - ES Modules: Padrão moderno, assíncrono, com import e export, recomendado para projetos novos em 2025. - Diferenças: Carregamento, sintaxe, live bindings, tree shaking e interoperabilidade. - Exemplo Prático: Criou um módulo personalizado (math) e o importou em ambos os formatos, testando diferenças como live bindings.

Este conhecimento é fundamental para organizar projetos Node.js de forma modular e escalável. Nos próximos módulos, você aplicará esses conceitos para construir APIs, manipular arquivos e integrar bancos de dados.

17.3.1 Próximos Passos

  • Experimente criar novos módulos personalizados (ex.: um módulo para strings ou cálculos avançados).
  • Explore a documentação de módulos no Node.js: nodejs.org/api/modules.html.
  • Prepare-se para o próximo módulo, onde abordaremos o Sistema de Arquivos (fs module).