O sistema de módulos define como arquivos compartilham funções, classes e dados. Os ES Modules no Node.js usam a sintaxe padronizada import e export, compatível com navegadores e ferramentas modernas. Eles coexistem com CommonJS, mas possuem regras diferentes para resolução, extensões, caminhos e carregamento.
Migrar apenas trocando require() por import pode quebrar o projeto. Em ESM, caminhos relativos exigem extensão, __dirname não existe diretamente e alguns pacotes exportam formatos diferentes. É importante entender como o Node.js identifica cada módulo.
Neste guia, você aprenderá a habilitar ESM, criar exports, usar import dinâmico, trabalhar com URLs, importar JSON, integrar CommonJS e organizar pacotes.
Habilitando ES Modules
A forma mais comum é definir type no package.json:
{
"type": "module"
}Arquivos .js passam a ser tratados como ESM. Outra opção é usar a extensão .mjs. Para forçar CommonJS dentro de um pacote ESM, use .cjs.
A documentação oficial de ES Modules detalha resolução e interoperabilidade. A organização de pacotes está na documentação de Packages.
Para revisar JavaScript e Node.js, consulte o que é JavaScript e o que é Node.js.
Export nomeado
export function sum(a, b) {
return a + b;
}
export const version = '1.0.0';No consumidor:
import { sum, version } from './math.js';O nome importado precisa corresponder ao exportado, salvo quando é usado as.
Export default
export default class UserService {
async findById(id) {
return database.users.findById(id);
}
}import UserService from './user-service.js';Um módulo possui no máximo um export default. Exports nomeados costumam facilitar refatoração e autocompletar quando há várias funcionalidades.
Extensões em caminhos relativos
import { sum } from './math.js';Em ESM, o Node.js normalmente exige a extensão completa em caminhos relativos. Isso segue o modelo de URLs e evita tentativas implícitas de vários arquivos.
Imports de módulos nativos
import fs from 'node:fs';
import { readFile } from 'node:fs/promises';O prefixo node: deixa claro que o módulo é nativo e evita conflito com pacotes de mesmo nome.
__dirname e __filename
ES Modules usam import.meta.url:
import { fileURLToPath } from 'node:url';
import path from 'node:path';
const filename = fileURLToPath(import.meta.url);
const dirname = path.dirname(filename);
const configFile = path.join(dirname, 'config.json');Em versões atuais, propriedades auxiliares podem existir, mas confirme a versão mínima antes de usá-las.
import.meta.url
O valor é uma URL completa do módulo. Ele permite localizar arquivos relativos sem depender do diretório atual do processo:
const templateUrl = new URL('./template.html', import.meta.url);
const template = await readFile(templateUrl, 'utf8');APIs de arquivo do Node.js aceitam URLs file: em muitos métodos. Veja File System no Node.js.
Import dinâmico
async function loadAdapter(name) {
const module = await import(`./adapters/${name}.js`);
return module.default;
}import() retorna uma Promise e pode ser usado condicionalmente. Não monte caminhos com entrada não confiável; use uma lista de adaptadores permitidos.
Top-level await
const config = await loadConfig();
export const application = createApplication(config);ESM permite await no nível superior. Use com cuidado: um módulo lento bloqueia a inicialização de todos os módulos que dependem dele. Prefira uma função explícita de bootstrap para operações complexas.
Importando JSON
A sintaxe e os atributos suportados dependem da versão do Node.js:
import config from './config.json' with {
type: 'json'
};Defina a versão mínima e teste o ambiente de produção. Para configurações mutáveis, ler com fs pode oferecer tratamento de erro mais claro.
Interoperabilidade com CommonJS
import legacyPackage from 'legacy-package';Ao importar CommonJS, o objeto module.exports costuma aparecer como default. Exports nomeados podem ser detectados por análise estática, mas não dependa disso sem testar.
Usando createRequire()
Quando um recurso só funciona com require():
import { createRequire } from 'node:module';
const require = createRequire(import.meta.url);
const legacyConfig = require('./legacy-config.cjs');Use como ponte de migração, não como padrão para todo o código.
Export map no package.json
{
"name": "my-library",
"type": "module",
"exports": {
".": "./src/index.js",
"./client": "./src/client.js"
}
}exports define a API pública e impede imports profundos não declarados. Isso permite reorganizar arquivos internos sem quebrar consumidores.
Exports condicionais
{
"exports": {
".": {
"import": "./dist/index.js",
"require": "./dist/index.cjs"
}
}
}Bibliotecas podem fornecer ESM e CommonJS. A ordem e as condições precisam ser testadas em ferramentas diferentes para evitar duas instâncias do mesmo estado.
O problema de pacote duplo
Quando uma biblioteca é carregada por import e require em formatos diferentes, caches separados podem criar duas cópias. Singletons e comparação de classes podem falhar. Centralize o estado em um módulo comum ou publique um formato quando possível.
Resolução de pacotes
Imports sem caminho relativo são resolvidos via pacotes e seus campos exports. Não dependa de arquivos internos não expostos:
import feature from 'package/internal/file.js';Esse caminho pode deixar de funcionar quando o pacote adiciona um export map.
Ciclos de dependência
ESM possui bindings vivos, mas ciclos ainda podem produzir valores não inicializados. Separe contratos, mova constantes compartilhadas ou injete dependências. Não resolva ciclos apenas mudando a ordem dos imports.
Tree shaking
Bundlers conseguem analisar imports estáticos e remover código não usado. O Node.js executando diretamente não faz tree shaking, mas a sintaxe estática beneficia ferramentas de build.
Testes
import test from 'node:test';
import assert from 'node:assert/strict';
import { sum } from './math.js';
test('sum', () => {
assert.equal(sum(2, 3), 5);
});O guia de Node Test Runner apresenta mocks e cobertura.
Migrando de CommonJS
Migre por etapas:
- defina a versão mínima do Node.js;
- identifique dependências CommonJS e ESM-only;
- adicione extensões a imports relativos;
- substitua
__dirnamee__filename; - revise carregamento de JSON;
- converta testes e scripts;
- valide ferramentas de build e lint;
- publique uma versão principal quando a mudança afetar consumidores.
Desempenho e cache
Módulos são avaliados uma vez por URL e reutilizados. URLs diferentes que apontam ao mesmo arquivo podem criar instâncias distintas em situações especiais. Evite parâmetros e fragmentos sem necessidade.
Segurança
Import dinâmico baseado em entrada pode carregar código inesperado. Use mapas explícitos:
const adapters = {
postgres: () => import('./adapters/postgres.js'),
memory: () => import('./adapters/memory.js')
};Erros comuns
- Esquecer extensão em import relativo.
- Usar
__dirnamediretamente. - Assumir que CommonJS possui exports nomeados confiáveis.
- Usar top-level await em inicialização lenta.
- Carregar caminho dinâmico do usuário.
- Publicar pacote sem export map estável.
- Misturar duas cópias de pacote dual.
- Migrar sem testar scripts e ferramentas.
Boas práticas
- Use
node:para módulos nativos. - Inclua extensões em imports relativos.
- Prefira exports nomeados para APIs amplas.
- Use
import.meta.urlpara recursos relativos. - Limite import dinâmico a opções conhecidas.
- Defina
exportsem bibliotecas. - Documente versão mínima do Node.js.
- Evite ciclos de dependência.
- Teste interoperabilidade CommonJS.
- Migre em etapas.
Conclusão
Os ES Modules no Node.js trazem a sintaxe padronizada de módulos para servidores e ferramentas. Eles oferecem imports estáticos, URLs, top-level await e integração com export maps.
A migração exige atenção a extensões, caminhos, JSON e CommonJS. Com uma API pública clara e versão mínima definida, ESM melhora a consistência entre Node.js, navegador e ferramentas modernas.




