Um módulo no Node é apenas um arquivo - mas um arquivo carregado sob regras que lhe dão seu próprio escopo privado, sua própria maneira de declarar o que ele compartilha e um caminho definido para como outros arquivos podem obtê-lo. Isso parece simples, e na maioria das vezes é, exceto que o Node na verdade suporta dois sistemas diferentes para essas regras - CommonJS e ES Modules - além de várias categorias distintas de módulo dependendo de onde o código vem.
Esta página é o mapa antes do território: Noções Básicas de Módulos percorre o código de trabalho para ambos os sistemas, e CommonJS (require) / ES Modules (import) aprofundam cada um em sua sintaxe e comportamento. Aqui, o objetivo é o modelo mental - o que realmente distingue um "tipo" de módulo e por que importa qual deles você está olhando.
Módulos do Node se dividem em dois eixos independentes - o sistema que os carrega (CommonJS ou ES Modules) e sua origem (core, local, terceiros, JSON ou nativo/WASM) - e ambos os eixos mudam como um determinado módulo realmente se comporta.
Por que Importa: A maioria dos bugs do tipo "por que esta importação não funciona" são realmente "estes são dois sistemas de módulos diferentes, não duas grafias da mesma coisa" - e saber com qual sistema e qual origem você está lidando diz o que é realmente possível.
Conceitos Chave:CommonJS (CJS), ES Modules (ESM), escopo de módulo, módulos core, especificador de módulo, package.json"type".
Quando Usar Este Modelo: Iniciar um novo projeto e decidir um padrão, depurar require is not defined ou Cannot use import statement outside a module, manter código que mistura módulos antigos e novos, ou publicar um pacote que precisa atender a ambos os tipos de consumidores.
Limitações / Trade-offs: Dois sistemas significam atrito real de interoperabilidade - carregamento síncrono vs. assíncrono, exportações estáticas vs. dinâmicas, e um risco genuíno de uma dependência ser carregada duas vezes como "dois módulos diferentes" se for puxada de ambas as maneiras.
Tópicos Relacionados: CommonJS require, ES Modules import, package.json"type" & exports, interop CJS/ESM, o algoritmo de resolução de módulos.
Antes que qualquer sistema de módulos existisse, cada arquivo JavaScript em um programa compartilhava um único escopo global - dois arquivos poderiam silenciosamente sobrescrever as variáveis um do outro apenas declarando x. O primeiro trabalho de um sistema de módulos é resolver isso: encapsular cada arquivo em seu próprio escopo privado, para que nada vaze para dentro ou para fora, exceto o que o arquivo compartilha explicitamente.
O Node precisava disso desde o primeiro dia (2009), anos antes do próprio JavaScript ter uma resposta oficial - então ele adotou o CommonJS, um sistema síncrono baseado em require(), construído para código do lado do servidor, baseado em sistema de arquivos. Quando o JavaScript mais tarde padronizou seu próprio sistema de módulos, ES Modules (import/export), o Node teve que adicionar suporte para um segundo sistema diferente ao lado do que já tinha - é por isso que o Node hoje roda em dois sistemas em vez de um.
Uma maneira simples de visualizar a diferença: CommonJS trata require() como entregar um bilhete a um assistente enquanto você já está trabalhando - "vá buscar aquele arquivo para mim" - e esperar ali mesmo até que ele volte, para que você possa chamá-lo em qualquer lugar, mesmo condicionalmente. ES Modules, em vez disso, trata import como submeter um manifesto de remessa antes do início do trabalho - todas as dependências são declaradas antecipadamente, em um local fixo, antes que uma única linha do próprio código do módulo seja executada.
// CJS: puxado dinamicamente, onde quer que você o chameconst math = require('./math.js');// ESM: declarado estaticamente, sempre no topo, antes que este arquivo seja executadoimport * as math from './math.js';
require.cache, indexado pelo caminho do arquivo resolvido
O próprio grafo de módulos; reimportar o mesmo especificador reutiliza a instância
Por origem, um módulo pode vir de cinco lugares diferentes, e essa origem - independentemente de qual sistema o carrega - determina como (ou se) ele é instalado:
Módulos Core / embutidos - compilados no próprio binário do Node (fs, http, path, …). Nada para instalar; o prefixo node: (node:fs) torna a origem inequívoca em qualquer sistema.
Módulos Locais / de arquivo - os arquivos do seu próprio projeto, resolvidos por caminho relativo ou absoluto.
Módulos de terceiros (npm) - instalados em node_modules, localizados pelo algoritmo de resolução do Node subindo na árvore de diretórios a partir do arquivo importador.
Módulos JSON - dados simples, não código - require() lê JSON implicitamente em CJS; ESM requer um atributo de importação explícito with { type: 'json' }.
Add-ons Nativos / Módulos WebAssembly - código de máquina compilado (C++/Rust via N-API, ou um binário .wasm) carregado através de um mecanismo diferente, expondo uma interface em forma de JS sem ser JavaScript em si.
Qual sistema se aplica a um determinado arquivo .js local não é adivinhação - o Node decide isso a partir do campo "type" do package.json ("module" vs. o padrão CommonJS) e da própria extensão do arquivo (.mjs e .cjs sempre substituem "type", forçando ESM ou CJS respectivamente, independentemente da configuração do pacote). package.json "type" & exports cobre essa decisão por completo; Algoritmo de Resolução de Módulos cobre como um especificador como 'lodash' ou './utils.js' realmente se transforma em um arquivo no disco.
Como CJS e ESM são sistemas de carregamento genuinamente diferentes, não apenas sintaxes diferentes, misturá-los tem falhas reais em vez de falhas cosméticas:
Preocupação
CommonJS
ES Modules
Tree-shaking (bundlers)
Ruim - require() dinâmico derrota a análise estática
Forte - importações estáticas são analisáveis antes da execução
Carregamento condicional
Natural - require() é apenas uma chamada de função
Requer import() dinâmico, que retorna uma Promise
await de nível superior
Não é possível - require é síncrono
Suportado nativamente
Risco de interop
Carregar o mesmo pacote via require e import pode instanciá-lo duas vezes - um verdadeiro "risco de pacote duplo" que quebra verificações instanceof e estado singleton compartilhado
Mesmo risco, da outra direção
Esse risco de pacote duplo é a ponta mais afiada em toda essa área: uma dependência carregada uma vez através de require() e uma vez através de import não tem garantia de ser a mesma instância de módulo, o que quebra silenciosamente qualquer coisa que dependa de igualdade de referência ou estado compartilhado em nível de módulo. Interop CJS ↔ ESM cobre createRequire, extensões explícitas .mjs/.cjs, e como evitá-lo durante uma migração.
Add-ons nativos e módulos WebAssembly ficam fora dessa comparação CJS/ESM - eles são carregados através de seus próprios bindings em vez de serem analisados como código-fonte JavaScript, mas ainda aparecem para o seu código como um objeto importado comum depois de carregados, é por isso que valem a pena ser mencionados como uma categoria, embora esta página não se aprofunde na construção de um.
Para código novo, a orientação atual é inequívoca: prefira ES Modules via "type": "module" em package.json. CommonJS não é depreciado e o Node não tem planos de removê-lo - o ecossistema é muito grande para isso - mas a estrutura estática do ESM é o que as ferramentas modernas (bundlers, type checkers, node --experimental-strip-types) estão cada vez mais preparadas para assumir.
"CommonJS e ES Modules são apenas duas sintaxes para a mesma coisa." Eles diferem em semânticas de carregamento, não em grafia - resolução síncrona vs. assíncrona do grafo, importações dinâmicas vs. estáticas, valores copiados vs. vínculos ao vivo.
"Um módulo Node sempre vem do npm." Muitos nunca tocam no node_modules - módulos core são enviados dentro do binário do Node, e arquivos locais são módulos no momento em que outro arquivo os importa.
"ESM é o moderno, então CommonJS está desaparecendo." CommonJS permanece totalmente suportado sem planos de remoção; ESM é o padrão recomendado para código novo, não uma substituição sendo implementada sob os pacotes existentes.
"Você pode misturar require e import livremente no mesmo arquivo." O sistema de um único arquivo é fixado por sua extensão e pelo campo "type" de seu pacote - você pode conectar os dois sistemas (createRequire, import() dinâmico), mas não pode declarar require e import de nível superior em um arquivo.
"Um arquivo .json importado em um módulo é basicamente JavaScript." São dados, não código - CJS o lê implicitamente via require(), enquanto ESM requer um atributo de importação explícito type: 'json', e nenhum dos sistemas o executa.
O que exatamente torna um arquivo um "módulo" no Node?
Qualquer arquivo carregado sob as regras de um sistema de módulos - recebendo seu próprio escopo, com uma maneira explícita de exportar valores e um caminho definido para outros arquivos importá-lo. Scripts de nível superior simples executados fora de um sistema de módulos não obtêm esse isolamento.
Por que o Node tem dois sistemas de módulos em vez de um?
CommonJS existia anos antes do JavaScript padronizar sua própria sintaxe de módulos, então o Node se baseou nele primeiro. Assim que ES Modules se tornou o sistema oficial da linguagem, o Node adicionou suporte para ele também - em vez de quebrar o enorme ecossistema CommonJS existente mudando completamente.
Módulos core como `fs` e `http` são um "tipo" diferente dos meus próprios arquivos?
Sim, por origem - módulos core são compilados no binário do Node e não precisam de instalação, ao contrário de arquivos locais ou pacotes npm. Eles ainda são carregados através do sistema (CJS ou ESM) que o arquivo importador usa.
Como o Node decide se um arquivo `.js` é CommonJS ou ESM?
Ele verifica primeiro a própria extensão do arquivo - .mjs sempre significa ESM, .cjs sempre significa CommonJS, independentemente de qualquer outra coisa. Para arquivos .js simples, ele olha para o campo "type" do package.json mais próximo, com o padrão CommonJS se esse campo estiver ausente.
Um único arquivo pode usar tanto `require` quanto `import`?
Não - o sistema de módulos de um arquivo é fixado por sua extensão/"type", e cada sistema reconhece apenas sua própria sintaxe. Você pode conectar os dois sistemas (createRequire para obter require dentro de um arquivo ESM, ou import() dinâmico dentro de um CJS), mas não pode misturar as formas estáticas import/export e require/module.exports no mesmo arquivo.
O que é um módulo JSON e por que ele precisa de sintaxe especial em ESM?
É um arquivo de dados simples (.json) tratado como um módulo importável em vez de código executável. CommonJS o lê implicitamente através de require(); ESM requer um atributo de importação explícito (with { type: 'json' }) porque, ao contrário do CJS, ESM precisa saber o tipo de conteúdo de um especificador antes de decidir como analisá-lo.
Arquivos de add-on nativos e WebAssembly são realmente "módulos"?
Eles se comportam como um do ponto de vista do seu código - você os importa/requer e recebe de volta um objeto em forma de JS - mas eles são carregados através de seu próprio mecanismo de binding em vez de serem analisados como código-fonte JavaScript, é por isso que são categorizados separadamente de CJS/ESM.
Qual é o "risco de pacote duplo" que devo observar?
É quando o mesmo pacote é carregado uma vez via require() e uma vez via import, produzindo duas instâncias de módulo separadas em vez de uma compartilhada - o que quebra silenciosamente verificações instanceof e qualquer estado que o módulo esperava ser um singleton. Interop CJS ↔ ESM cobre como evitá-lo.
Um novo projeto deve usar CommonJS ou ES Modules?
ES Modules, via "type": "module" em package.json. É o sistema padrão da linguagem, tem suporte de ferramentas mais forte (tree-shaking, análise estática, await de nível superior) e é a direção para a qual o ecossistema está se movendo ativamente - CommonJS permanece totalmente suportado para código existente e legado.
Por que o tree-shaking funciona melhor com ES Modules?
Bundlers só podem remover código não utilizado com segurança quando podem provar, estaticamente, o que um módulo importa e exporta - o que as declarações import/export fixas no topo do arquivo do ESM garantem. O require() do CommonJS é uma chamada de função em tempo de execução, então um bundler nem sempre pode dizer o que ele carrega sem realmente executar o código.
O `import()` (a forma de função) pertence ao ESM ou CommonJS?
Ambos podem usá-lo - import() dinâmico é uma função que retorna uma Promise, disponível mesmo dentro de arquivos CommonJS, especificamente para que qualquer sistema possa carregar um módulo condicionalmente ou assincronamente sem precisar de uma declaração import estática.