Módulos em JavaScript: import e export (ES Modules)
Named e default export, live bindings, escopo de módulo e por que ES Modules não abrem em file:// — com os dois erros que travam todo mundo reproduzidos.
Módulo é um arquivo que decide o que mostra para fora. Tudo que você declara
nele fica privado por padrão; só o que leva export fica visível, e só quem
escreve import enxerga. É assim que um projeto para de ser um punhado de
<script> disputando as mesmas variáveis globais.
A resposta curta: export marca o que sai, import traz o que entra, o caminho
relativo precisa da extensão (./frete.mjs, não ./frete), e no navegador a
tag tem que ser <script type="module"> servida por http, porque abrir o
HTML direto do disco não funciona. O resto deste texto mostra cada regra
rodando, incluindo as duas em que todo mundo tropeça.
Os exemplos são de uma loja virtual dividida em três arquivos: frete, carrinho e cupons.
Portas e balcões: o modelo mental de um módulo
Imagine que cada arquivo é uma sala de uma loja. O que fica dentro da sala não
atrapalha ninguém do lado de fora. Quando algo precisa ser usado no caixa, a
sala coloca esse item no balcão com export; o caixa o recolhe com import.
Sem balcão, o item continua existindo, mas só naquela sala.
Essa imagem volta diretamente ao mecanismo técnico: módulos têm escopo
próprio, export define a interface pública e import cria uma ligação com o
valor exportado. Na microprática abaixo, retire mentalmente um export antes de
rodar e preveja a mensagem; depois confira no terminal qual nome o motor diz que
o módulo não oferece.
O arquivo frete.mjs declara três coisas e exporta as três:
export const TABELA = { sudeste: 12.5, sul: 18.9, nordeste: 27.4, norte: 34.9 };
export function calcularFrete(peso, regiao = 'sudeste') {
return (TABELA[regiao] ?? 39.9) + peso * 1.5;
}
export const FRETE_GRATIS_ACIMA_DE = 199;E app.mjs traz só o que precisa:
import { calcularFrete, FRETE_GRATIS_ACIMA_DE } from './frete.mjs';
console.log('frete sudeste:', calcularFrete(2));
console.log('frete norte :', calcularFrete(2, 'norte'));
console.log('grátis acima de R$', FRETE_GRATIS_ACIMA_DE);Três detalhes que economizam meia hora de confusão:
- As chaves em
import { ... }não são desestruturação de objeto. É sintaxe própria de módulo, e o nome dentro delas tem que bater exatamente com o nome exportado. - O
./é obrigatório. Sem ele,import { x } from 'frete.mjs'procura um pacote instalado, não o arquivo ao lado. - A extensão também é obrigatória no Node e no navegador. O especificador é uma
URL, e URL não tenta
.js,.mjs,/index.jspor conta própria.
Default, renomeação e namespace
Um arquivo pode ter um export default — o valor principal dele. Aqui,
a classe Carrinho:
export default class Carrinho {
constructor(cliente) {
this.cliente = cliente;
this.itens = [];
}
adicionar(item) {
this.itens.push(item);
return this;
}
get subtotal() {
return this.itens.reduce((soma, i) => soma + i.preco, 0);
}
}
export const LIMITE_DE_ITENS = 20;Quem importa escolhe o nome do default — e pode misturar com os nomeados, trocar
o nome de um deles com as ou trazer o módulo inteiro como objeto:
import Carrinho, { LIMITE_DE_ITENS } from './carrinho.mjs';
import * as frete from './frete.mjs';
import { calcularFrete as freteDe } from './frete.mjs';
const c = new Carrinho('Ana')
.adicionar({ nome: 'Teclado mecânico', preco: 289.9 })
.adicionar({ nome: 'Mouse sem fio', preco: 149.9 });
console.log('cliente :', c.cliente);
console.log('subtotal:', c.subtotal.toFixed(2));
console.log('limite :', LIMITE_DE_ITENS);
console.log('namespace:', Object.keys(frete));
console.log('renomeado:', freteDe(1, 'sul'));Repare que Object.keys do namespace veio em ordem alfabética, não na ordem em
que os export aparecem no arquivo — a especificação exige isso.
O import é içado e roda antes do seu código
Um import não é executado na linha em que está escrito. O motor lê todos os
import do arquivo, carrega e avalia cada dependência antes de rodar a
primeira linha do módulo que importa. Dá para provar:
console.log('[app] primeira linha do arquivo');
import { NOME } from './banner.mjs';
console.log('[app] loja:', NOME);Com banner.mjs sendo:
console.log('[banner] módulo avaliado');
export const NOME = 'Club Store';A mensagem do banner apareceu antes da “primeira linha do arquivo”, mesmo
com o import escrito depois dela. É um içamento parecido com o de declaração
de função, descrito em variáveis em JavaScript,
só que valendo para o arquivo inteiro.
Consequência prática: import só existe no topo do módulo, nunca dentro de
if, de função ou de bloco. Para carregar condicionalmente existe o import()
dinâmico, mais abaixo.
Live bindings: o import não é uma cópia
Esta é a diferença mais importante entre import e o antigo require, e a que
quase ninguém sabe. O que você importa é uma ligação viva com a variável do
outro módulo, não uma fotografia do valor dela:
export let unidades = 3;
export function vender(quantidade) {
unidades -= quantidade;
}import { unidades, vender } from './estoque.mjs';
console.log('antes :', unidades);
vender(2);
console.log('depois:', unidades);
try {
unidades = 10;
} catch (erro) {
console.log(erro.name, '|', erro.message);
}Duas coisas aconteceram. unidades mudou de 3 para 1 do lado de fora, sem
reimportar nada — a ligação acompanha a variável original. E a tentativa de
atribuir de fora falhou com Assignment to constant variable, mesmo o módulo
tendo declarado com let: do lado de quem importa, todo binding é somente
leitura.
Quem muda o valor é o módulo dono dele, por meio de uma função exportada. É um modelo bem mais previsível do que passar o objeto inteiro e deixar qualquer arquivo escrever nele.
Escopo próprio e modo estrito de graça
Módulo não polui o global e é sempre strict mode, sem precisar do
'use strict':
const precoBase = 289.9;
console.log('globalThis.precoBase:', globalThis.precoBase);
console.log('this no topo :', this);
totalDoCarrinho = 999;Três fatos numa saída. A const do topo não virou propriedade global. O
this no topo de um módulo é undefined (num script clássico seria window).
E atribuir a um nome não declarado lança ReferenceError em vez de criar uma
global silenciosa — comportamento do modo estrito, que aqui vem ligado por
padrão.
Isso muda o que você pode esperar de código copiado da internet: qualquer
tutorial que dependa de “a função fica disponível no window” não funciona
dentro de um módulo.
No navegador: type=module e o bloqueio do file://
No HTML, um módulo entra assim:
<script type="module" src="./app.js"></script>O type="module" muda quatro comportamentos de uma vez: o arquivo pode usar
import, ele é adiado automaticamente (não precisa de defer), roda em modo
estrito e ganha escopo próprio.
E aí vem a pedra no caminho de todo iniciante: abrir o index.html com dois
cliques não funciona. O endereço vira file:///Users/ana/loja/index.html, cuja
origem é null, e o navegador trata cada import como uma requisição entre
origens. O Chrome responde no console:
Não é bug e não tem --flag que valha a pena. A solução é servir por HTTP,
e qualquer uma destas resolve em dez segundos:
npx serve .
python3 -m http.server 8000Com o site em http://localhost:8000, o mesmo arquivo carrega sem tocar em uma
linha de código. No VS Code, a extensão Live Server faz o mesmo com um clique.
O erro que aparece no Node: import statement outside a module
O Node decide se um arquivo é ESM ou CommonJS pela extensão e pelo
package.json. .mjs é sempre ESM, .cjs é sempre CommonJS, e .js depende
do campo type do package.json mais próximo.
Desde o Node 22.7 existe detecção automática de sintaxe: um .js com import é
tratado como ESM mesmo sem "type": "module". Por isso, para reproduzir o erro
clássico, é preciso um contexto CommonJS explícito — um package.json com
"type": "commonjs", que é o caso de milhares de projetos antigos:
import { calcularFrete } from './frete.mjs';
console.log(calcularFrete(2));O próprio aviso entrega as duas saídas: trocar o type para module no
package.json, ou renomear o arquivo para .mjs. Repare também que a stack
inteira vem de node:internal/modules/cjs/loader — a pista de que o arquivo foi
carregado pelo caminho CommonJS, e não pelo de módulo.
O erro espelhado acontece quando você escreve require dentro de um ESM:
const { calcularFrete } = require('./frete.mjs');
console.log(calcularFrete(2));E há um terceiro, bem mais amigável do que parece: importar um nome que o outro arquivo não exporta.
import { calcularFreteGratis } from './frete.mjs';
console.log(calcularFreteGratis(2));É SyntaxError e acontece antes de qualquer linha rodar, porque o motor
liga todos os módulos na fase de instanciação. Esse é o ganho real de escrever
import em vez de require: o typo aparece no carregamento, não na hora em que
o usuário clica no botão. Vale reler o que cada família de erro significa em
tipos de erro em JavaScript.
ESM contra CommonJS
| ES Modules | CommonJS | |
|---|---|---|
| sintaxe | import / export |
require / module.exports |
| quando resolve | na análise, antes de rodar | na execução, na linha do require |
| o que você recebe | ligação viva, somente leitura | cópia do valor no momento |
| carregamento | assíncrono | síncrono |
| roda no navegador | sim | não, sem bundler |
import condicional |
só com import() |
require dentro de if funciona |
Na prática você vai conviver com os dois por anos. Um ESM consegue importar um
arquivo CommonJS: o module.exports chega como default, e o Node ainda tenta
adivinhar os nomeados:
const CUPONS = { PRIMEIRACOMPRA: 10, BLACKFRIDAY: 30 };
function aplicar(valor, codigo) {
return valor - valor * ((CUPONS[codigo] ?? 0) / 100);
}
module.exports = { CUPONS, aplicar };import cupons from './cupons.cjs';
import { aplicar } from './cupons.cjs';
console.log('default :', Object.keys(cupons));
console.log('nomeado :', aplicar(200, 'BLACKFRIDAY'));O caminho inverso — CommonJS importando ESM — não funciona com require porque
ESM é assíncrono. Ali só o import() dinâmico resolve.
import() dinâmico
import() é uma função que devolve promessa. Serve para carregar um módulo em
tempo de execução, com o caminho decidido na hora, e é a base do code
splitting de qualquer front-end moderno:
const REGIAO = process.env.REGIAO ?? 'sudeste';
const { calcularFrete } = await import('./frete.mjs');
console.log('dinâmico:', calcularFrete(2, REGIAO));
const carrinho = await import('./carrinho.mjs');
console.log('chaves do módulo:', Object.keys(carrinho));
const Carrinho = carrinho.default;
console.log('instância:', new Carrinho('Bruno').cliente);Repare que o default vem como uma propriedade normal do objeto resolvido. E
que o await está no topo do arquivo, sem função async em volta: top-level
await é exclusividade de módulo ESM.
Como dividir um projeto de verdade
Módulo bem desenhado não é módulo pequeno; é módulo com uma responsabilidade que cabe no nome do arquivo. Quatro hábitos que sustentam projeto grande:
- Um assunto por arquivo, nomeado pelo assunto.
frete.js,carrinho.js,cupons.js. Se o nome precisa de “e”, são dois arquivos. - Named export por padrão. Você ganha autocomplete, renomeação segura e
erro de import na hora do carregamento em vez de
undefinedno meio da tela. - Nada de efeito colateral no topo. Um
fetchou umdocument.querySelectorsolto no corpo do módulo roda no instante em que alguém o importa — e você perde o controle da ordem. Exporte uma funçãoiniciar()e chame-a de um lugar só. - Evite ciclos.
a.jsimportandob.jsque importaa.jsnão quebra na hora, mas entregaundefinednum dos lados durante a avaliação. Quando acontecer, odebuggerno topo de cada módulo mostra a ordem real — e o caminho para investigar isso está em debugar JavaScript no DevTools.
Com o projeto em módulos, a próxima parada da trilha de JavaScript é o tratamento de texto com expressões regulares. E se quiser rever a ordem de estudo inteira, o guia completo de JavaScript tem o mapa.
Perguntas frequentes
Devo usar export default ou named export?
Preciso escrever a extensão .js no import?
Por que meu HTML não carrega o módulo abrindo o arquivo direto?
Posso misturar require e import no mesmo projeto?
Dúvidas e comentários
Travou em algum passo? Pergunte aqui — a equipe e outros alunos respondem.
Entrar para perguntarÉ o mesmo login gratuito dos cursos.
Nenhuma dúvida por aqui ainda — a primeira pode ser a sua.
Todo o código deste artigo foi executado em Node 24.16.0, e as saídas exibidas são as reais — como produzimos este conteúdo.
Fontes consultadas
- MDN — Módulos JavaScript — developer.mozilla.org
- Node.js — Modules: ECMAScript modules — nodejs.org
- ECMAScript 2026 Language Specification — Modules — tc39.es


