Durante muitos anos, o CommonJS foi o principal sistema de módulos do Node.js. Ele usa require() para carregar dependências e module.exports para publicar valores. Mesmo com a adoção de ES Modules, uma grande quantidade de aplicações, scripts e pacotes ainda depende do CommonJS no Node.js.
O formato parece simples, mas possui detalhes importantes: módulos são encapsulados, carregados de forma síncrona e armazenados em cache. Alterar exports incorretamente, criar ciclos ou misturar CommonJS e ESM sem entender a interoperabilidade pode causar resultados inesperados.
Neste guia, você aprenderá a criar módulos, usar exports, entender resolução e cache, carregar JSON, lidar com ciclos, invalidar cache em testes e migrar gradualmente para ES Modules.
Primeiro módulo CommonJS
// math.js
function sum(a, b) {
return a + b;
}
module.exports = {
sum
};No consumidor:
const { sum } = require('./math');
console.log(sum(2, 3));A extensão .js pode ser omitida em muitos imports CommonJS. O Node.js aplica regras de resolução para encontrar arquivo ou diretório.
A documentação oficial de CommonJS explica resolução, wrapper e cache. Para comparar com o padrão moderno, veja a documentação de ES Modules.
Consulte também o que é Node.js e o que é JavaScript.
module.exports e exports
exports começa como uma referência para module.exports:
exports.sum = (a, b) => a + b;
exports.subtract = (a, b) => a - b;Porém, atribuir outro valor diretamente a exports quebra a referência:
exports = function service() {};Esse código não substitui o valor exportado. Para publicar uma função única:
module.exports = function service() {
return 'ok';
};O wrapper do módulo
Antes da execução, o Node.js envolve o arquivo em uma função semelhante a:
(function(exports, require, module, __filename, __dirname) {
// conteúdo do arquivo
});Por isso, variáveis declaradas no nível superior não ficam globais e os identificadores __filename e __dirname estão disponíveis.
Caminhos relativos
const config = require('./config');
const helper = require('../shared/helper');Caminhos iniciados por ponto são resolvidos em relação ao arquivo atual, não ao diretório de execução. Para arquivos de dados, use path.join(__dirname, ...):
const path = require('node:path');
const file = path.join(__dirname, 'templates', 'email.html');O artigo de File System no Node.js mostra como validar caminhos.
Módulos nativos
const fs = require('node:fs');
const path = require('node:path');O prefixo node: deixa explícito que a dependência pertence à plataforma.
Pacotes de node_modules
const express = require('express');O Node.js procura o pacote em diretórios node_modules ascendentes. O campo exports do pacote pode restringir subcaminhos disponíveis.
Carregando JSON
const config = require('./config.json');O JSON é lido, analisado e armazenado em cache. Alterar o arquivo no disco não atualiza automaticamente o objeto. Para configuração que muda em tempo de execução, leia com fs e valide o schema.
Cache de módulos
const first = require('./service');
const second = require('./service');
console.log(first === second); // trueApós a primeira execução, o valor exportado é reutilizado. Isso permite singletons, mas também mantém estado mutável entre consumidores e testes.
Estado compartilhado
// counter.js
let value = 0;
module.exports = {
increment() {
value++;
},
get() {
return value;
}
};Todos os consumidores da mesma instância do módulo enxergam o contador. Prefira factories quando cada contexto precisa de estado isolado:
module.exports = function createCounter() {
let value = 0;
return {
increment: () => ++value,
get: () => value
};
};Inspecionando e removendo cache
const id = require.resolve('./config');
delete require.cache[id];Remover uma entrada não descarrega automaticamente toda a árvore de dependências. Use esse recurso com cautela, geralmente em ferramentas e testes. Em aplicações, prefira APIs explícitas de reload.
require.resolve()
const location = require.resolve('some-package');
console.log(location);A função informa o arquivo que seria carregado sem executá-lo. Isso é útil em diagnóstico e plugins, mas não deve ser usado para acessar internals não públicos.
Carregamento síncrono
require() carrega e avalia o módulo de forma síncrona. Dependências no início da aplicação são adequadas, mas carregar código pesado durante uma requisição pode aumentar latência.
Para carregamento assíncrono e condicional, CommonJS pode usar import():
async function loadModule() {
const module = await import('./modern-module.js');
return module.default;
}Ciclos de dependência
Quando A requer B e B requer A, um deles pode receber exports parcialmente inicializados:
// a.js
exports.ready = false;
const b = require('./b');
exports.ready = true;Ciclos tornam a ordem de inicialização difícil de prever. Extraia contratos compartilhados, use injeção de dependência ou reorganize responsabilidades.
Interoperabilidade com ES Modules
Um ES Module pode importar CommonJS como default:
import legacy from './legacy.cjs';Um módulo CommonJS não pode usar require() diretamente para carregar todo ES Module. Use import() assíncrono:
const modern = await import('./modern.js');Veja o guia de ES Modules no Node.js.
main e exports no package.json
{
"main": "./index.js",
"exports": {
".": "./index.js",
"./client": "./client.js"
}
}main define a entrada tradicional. exports controla a API pública e pode bloquear imports profundos.
Pacote dual
Uma biblioteca pode oferecer arquivos para require e import. Contudo, duas versões podem criar instâncias separadas do estado interno. Teste singletons, classes e símbolos compartilhados.
Plugins
Não aceite um nome arbitrário e passe para require(). Use um mapa permitido:
const plugins = {
csv: './plugins/csv',
json: './plugins/json'
};
function loadPlugin(name) {
const target = plugins[name];
if (!target) throw new Error('Plugin inválido');
return require(target);
}Isso evita carregar arquivos ou pacotes inesperados.
Mocks e testes
Como dependências são capturadas ao carregar o módulo, substituir um objeto depois pode não afetar referências internas. Prefira injeção:
module.exports = function createService({ repository, logger }) {
return {
async run() {
const data = await repository.load();
logger.info('loaded');
return data;
}
};
};O Node Test Runner oferece mocks e hooks.
Migrando para ESM
Uma migração segura inclui:
- definir a versão mínima do Node.js;
- identificar pacotes ESM-only;
- trocar exports e imports por etapas;
- renomear pontes para
.cjs; - adicionar extensões aos imports ESM;
- substituir
__dirnamequando necessário; - validar scripts, testes e ferramentas;
- revisar o contrato público da biblioteca.
Quando manter CommonJS
Projetos estáveis, scripts legados e ambientes com ferramentas antigas podem continuar usando CommonJS. Não migre apenas por moda. Avalie compatibilidade, manutenção e benefícios reais.
Erros comuns
- Atribuir valor diretamente a
exports. - Depender de estado global no cache.
- Remover cache sem entender dependências.
- Criar ciclos entre módulos.
- Carregar entrada do usuário com
require(). - Assumir que JSON será recarregado.
- Usar imports profundos de pacote.
- Misturar ESM e CommonJS sem testes.
Boas práticas
- Use
module.exportspara substituir o valor. - Prefira factories para estado isolado.
- Use
node:em módulos nativos. - Evite ciclos.
- Defina a API pública com
exports. - Não carregue caminhos arbitrários.
- Injete dependências para testar.
- Documente efeitos do cache.
- Use
import()para ESM. - Migre somente com plano e testes.
Conclusão
O CommonJS no Node.js continua relevante em aplicações e pacotes. Seu modelo síncrono, cache e sintaxe simples tornam o formato previsível quando suas regras são compreendidas.
O cuidado principal está em exports, estado compartilhado e interoperabilidade. Com módulos pequenos, factories e uma API pública clara, CommonJS pode coexistir com ES Modules durante uma migração gradual ou permanecer como base de projetos estáveis.


