Home > Blog > Desenvolvimento Web
Desenvolvimento Web
JavaScript
Programação

ESM no Node.js: import, export e top-level await

Atualizado em: 10 de outubro de 2026

Estação de trabalho usada no desenvolvimento de aplicações Node.js

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.cjs

Em 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.js

Separe 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

  1. adicione testes antes da mudança;
  2. mapeie usos de require, module.exports e variáveis globais;
  3. converta módulos sem efeitos colaterais primeiro;
  4. adicione extensões aos imports relativos;
  5. substitua caminhos por import.meta.url;
  6. adapte ferramentas de teste e lint;
  7. migre por pacote, não por arquivos aleatórios;
  8. execute a aplicação em ambiente semelhante à produção.

Erros comuns

  • esquecer a extensão em import relativo;
  • misturar module.exports com export no mesmo arquivo;
  • esperar __dirname global;
  • 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: module explicitamente;
  • 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.

10 melhores cursos de programação em 2026

Descubra os melhores cursos de programação. Aprenda a escolher o curso ideal para iniciar ou avançar na carreira de desenvolvedor

POSTS RELACIONADOS

Ver todos

Seta para a direita