ESM no Node.js é o sistema moderno de módulos baseado nas palavras-chave import e export. Ele permite organizar aplicações em arquivos menores, declarar dependências de forma estática, reutilizar código e compartilhar bibliotecas entre projetos JavaScript.
Embora o CommonJS continue presente no ecossistema, novos projetos se beneficiam de uma estratégia explícita para ECMAScript Modules. O objetivo não é apenas trocar require() por import, mas compreender resolução de arquivos, extensões obrigatórias, interoperabilidade, carregamento assíncrono e publicação de pacotes.
Neste guia, você aprenderá a ativar ESM, importar arquivos locais e pacotes, trabalhar com caminhos, JSON, módulos internos, testes, migração gradual e boas práticas para produção.
O que é ESM?
ECMAScript Modules é o padrão oficial de módulos da linguagem JavaScript. Um arquivo pode exportar valores e outro pode importá-los por meio de uma sintaxe declarativa.
// math.js
export function somar(a, b) {
return a + b;
}
// app.js
import { somar } from './math.js';
console.log(somar(20, 22));A dependência aparece no início do arquivo e pode ser analisada antes da execução. Isso favorece ferramentas de build, análise estática e organização de código.
Ativando ESM com type module
A forma mais comum é definir type como module no package.json:
{
"name": "api-esm",
"version": "1.0.0",
"type": "module"
}A partir daí, arquivos .js do pacote são interpretados como ESM. Sem essa configuração, .js normalmente segue o modo CommonJS.
Usando as extensões mjs e cjs
A extensão .mjs força ESM, enquanto .cjs força CommonJS. Elas são úteis em migrações e projetos que precisam manter os dois sistemas.
src/
server.mjs
legacy.cjsEm projetos novos, uma configuração única com type: module costuma ser mais simples. Use extensões especiais apenas quando houver uma necessidade real.
Extensão em imports locais
No ESM do Node.js, imports relativos devem indicar a extensão do arquivo:
import { criarServidor } from './server.js';Evite omitir .js esperando que o runtime tente várias alternativas. A resolução explícita reduz ambiguidades e torna o comportamento consistente.
Exports nomeados
export const porta = 3000;
export function iniciar() {
console.log(`Servidor na porta ${porta}`);
}O consumidor importa apenas os nomes necessários:
import { porta, iniciar } from './config.js';Export default
export default class UsuarioService {
async buscar(id) {
return { id };
}
}import UsuarioService from './usuario-service.js';Use export default quando o módulo tiver um conceito principal. Para bibliotecas com várias funções, exports nomeados geralmente facilitam refatoração e descoberta.
Renomeando imports
import { buscar as buscarUsuario } from './usuarios.js';O alias evita conflito entre nomes e mantém o código expressivo.
Reexportando módulos
Um arquivo de entrada pode reunir exports de vários arquivos:
// index.js
export { criarUsuario } from './criar-usuario.js';
export { buscarUsuario } from './buscar-usuario.js';
export { removerUsuario } from './remover-usuario.js';Esse padrão simplifica a API pública, mas evite barrels gigantes que escondem dependências ou criam ciclos difíceis de diagnosticar.
Importando módulos nativos
Prefira o prefixo node: para deixar claro que a dependência pertence ao runtime:
import { readFile } from 'node:fs/promises';
import { createServer } from 'node:http';
import path from 'node:path';O prefixo diferencia módulos internos de pacotes instalados com nomes semelhantes.
__dirname e __filename no ESM
As variáveis globais do CommonJS não existem diretamente. Use import.meta.url:
import { fileURLToPath } from 'node:url';
import path from 'node:path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);Quando você só precisa de um arquivo relativo ao módulo, use new URL():
const arquivo = new URL('./data/config.json', import.meta.url);Top-level await
ESM permite usar await no nível superior:
const config = await carregarConfiguracao();
await conectarBanco(config.databaseUrl);
iniciarServidor();O recurso facilita inicialização, mas módulos que aguardam recursos lentos atrasam toda a cadeia dependente. Defina timeouts e falhe de maneira clara.
Import dinâmico
Use import() quando o módulo só deve ser carregado em determinada condição:
const nome = process.env.RELATORIO;
if (nome === 'pdf') {
const { gerarPdf } = await import('./pdf.js');
await gerarPdf();
}O import dinâmico também funciona em CommonJS e retorna uma Promise. Consulte o artigo Dynamic Import no Node.js quando estiver publicado.
Importando JSON
Uma alternativa estável e explícita é ler o arquivo com fs:
import { readFile } from 'node:fs/promises';
const raw = await readFile(new URL('./config.json', import.meta.url), 'utf8');
const config = JSON.parse(raw);Valide o conteúdo antes de usar. O artigo JSON Schema no Node.js mostra como transformar configuração externa em um contrato verificável.
Interoperabilidade com CommonJS
Um módulo ESM pode importar muitos pacotes CommonJS:
import pacote from 'pacote-commonjs';Exports nomeados sintetizados podem variar conforme o pacote. Quando houver dúvida, importe o valor default e inspecione a API documentada.
Usando createRequire
Quando uma dependência ou recurso ainda exige require():
import { createRequire } from 'node:module';
const require = createRequire(import.meta.url);
const dados = require('./dados.json');Use essa ponte de forma localizada. Espalhar createRequire por toda a aplicação torna a migração difícil.
Ciclos de dependência
Um ciclo ocorre quando A importa B e B importa A. ESM mantém bindings vivos, mas a ordem de inicialização ainda pode produzir valores indisponíveis.
Reduza ciclos extraindo contratos e utilitários para módulos independentes. Dependências circulares frequentes normalmente indicam responsabilidades misturadas.
Estrutura recomendada
src/
app.js
server.js
config/
env.js
modules/
users/
controller.js
service.js
repository.js
shared/
errors.js
logger.jsSepare criação da aplicação, inicialização do servidor e infraestrutura. Essa divisão melhora testes e graceful shutdown.
Testando módulos ESM
O test runner nativo aceita ESM quando o projeto está configurado:
import test from 'node:test';
import assert from 'node:assert/strict';
import { somar } from '../src/math.js';
test('soma dois números', () => {
assert.equal(somar(2, 3), 5);
});Evite executar efeitos colaterais no momento do import. Exporte funções de fábrica para que testes possam fornecer dependências controladas.
Variáveis de ambiente
Carregue e valide configuração em um único módulo. O guia Variáveis de Ambiente no Node.js mostra parsing, defaults e proteção de dados sensíveis.
Publicando pacotes ESM
Defina uma API pública pequena com o campo exports:
{
"type": "module",
"exports": {
".": "./dist/index.js",
"./errors": "./dist/errors.js"
}
}Não dependa de caminhos internos do pacote. O artigo sobre Package Exports no Node.js aprofunda encapsulamento e subpaths.
Migração de CommonJS para ESM
- adicione testes antes da mudança;
- mapeie usos de
require,module.exportse variáveis globais; - converta módulos sem efeitos colaterais primeiro;
- adicione extensões aos imports relativos;
- substitua caminhos por
import.meta.url; - adapte ferramentas de teste e lint;
- migre por pacote, não por arquivos aleatórios;
- execute a aplicação em ambiente semelhante à produção.
Erros comuns
- esquecer a extensão em import relativo;
- misturar
module.exportscomexportno mesmo arquivo; - esperar
__dirnameglobal; - usar top-level await sem timeout;
- importar caminhos internos de dependências;
- criar barrels que geram ciclos;
- executar conexão com banco durante o import;
- presumir que todo pacote CommonJS oferece exports nomeados.
Checklist para produção
- defina
type: moduleexplicitamente; - use extensões em caminhos relativos;
- prefira
node:para módulos internos; - centralize configuração;
- evite efeitos colaterais no import;
- limite a API pública com exports;
- teste interoperabilidade;
- monitore tempo de inicialização;
- documente a versão mínima do Node.js.
Conclusão
ESM no Node.js oferece uma base padronizada para organizar aplicações e bibliotecas modernas. A sintaxe declarativa melhora legibilidade, enquanto recursos como top-level await, import dinâmico e import.meta.url atendem necessidades comuns do backend.
A adoção deve ser planejada. Defina o tipo do pacote, mantenha imports explícitos, elimine efeitos colaterais e trate a interoperabilidade como uma etapa de migração. Com uma API pública pequena e testes adequados, ESM torna o projeto mais previsível e fácil de evoluir.
Consulte a documentação oficial de ECMAScript Modules do Node.js e a referência de módulos JavaScript da MDN.




