Menu

CommonJS vs ES Modules: require ou import no Node?

Os dois sistemas de módulos do JavaScript, por que ambos ainda existem e como decidir entre require e import nos seus projetos Node.

Dois sistemas de módulos, uma única linguagem

No começo, o JavaScript não tinha nenhum sistema de módulos. O Node resolveu esse problema em 2009 com o CommonJS (require, module.exports), e durante anos foi assim que o código Node era escrito. Só em 2015 a própria linguagem ganhou um sistema de módulos padronizado — os ES Modules (import, export) — hoje suportado tanto pelos navegadores quanto pelo Node.

Por isso você vai encontrar os dois por aí. Veja o mesmo módulo simples escrito nas duas formas:

index.js
Output
Click Run to see the output here.
index.js
Output
Click Run to see the output here.

Mesma função, embalagens diferentes. O resto desta página é sobre quando cada embalagem importa e qual delas escolher.

Diferenças de sintaxe entre CommonJS e ES Modules

As diferenças do dia a dia cabem num cartão postal:

// CommonJS
const fs = require("fs");
const { readFile } = require("fs/promises");

module.exports = something;
module.exports.name = value;
exports.name = value;
// ES Modules
import fs from "fs";
import { readFile } from "fs/promises";

export default something;
export const name = value;
export { name };

require é uma chamada de função comum. Já o import é uma declaração — só pode aparecer no topo do módulo, e o caminho precisa ser uma string literal. Essa restrição não é frescura: é justamente o que permite aos ES Modules fazerem coisas que o CommonJS não consegue.

A diferença que realmente importa: estático vs dinâmico

No CommonJS, o require() é avaliado na hora em que a linha executa. Dá pra colocar dentro de um if, montar o caminho em tempo de execução, carregar o módulo só sob certas condições:

index.js
Output
Click Run to see the output here.

Os ES Modules são estáticos. O engine faz o parse de todos os import antes de executar qualquer linha de código, monta um grafo de dependências e resolve tudo de antemão. É por isso que o caminho precisa ser uma string literal e o import só pode aparecer no topo do arquivo.

A vantagem disso é grande: as ferramentas conseguem enxergar o grafo inteiro de módulos sem precisar executar nada. É assim que os bundlers fazem tree-shaking (descartando exports não usados), que os editores entregam autocomplete preciso e que o navegador consegue baixar módulos em paralelo.

Agora, quando você realmente precisa carregar algo dinamicamente em ESM, entra em cena o import() — uma expressão parecida com uma função que devolve uma Promise:

index.js
Output
Click Run to see the output here.

Como o Node decide qual sistema cada arquivo usa

Um mesmo projeto Node pode ter arquivos dos dois tipos convivendo sem problema. Para saber qual sistema aplicar em cada arquivo, o Node olha para duas coisas:

  • A extensão do arquivo: .mjs é sempre ESM e .cjs é sempre CommonJS.
  • O campo "type" do package.json mais próximo: se for "module", os arquivos .js viram ESM; se for "commonjs" (o padrão, quando o campo não existe), eles são tratados como CJS.
// package.json
{
    "name": "my-app",
    "type": "module"
}

Com "type": "module" no package.json, qualquer hello.js do mesmo pacote já usa import/export. Coloque um hello.cjs do lado e esse arquivo específico volta a usar require. É assim que dá para migrar um projeto aos poucos — ou publicar uma lib que entrega as duas versões ao mesmo tempo.

Pegadinha clássica para quem está começando: dentro de um arquivo ESM, require e module.exports simplesmente não existem. Se você digitar no automático, leva um ReferenceError na cara.

Interop: misturando CommonJS e ES Modules

Cedo ou tarde você vai precisar que um arquivo ESM consuma um pacote CommonJS, ou o contrário. E as regras não são simétricas.

ESM importando CommonJS funciona direto. O objeto module.exports do CJS vira o default export do módulo:

index.js
Output
Click Run to see the output here.
// app.mjs
import greet from "./greet.cjs";
console.log(greet("Rosa"));

Os imports nomeados a partir de módulos CommonJS até funcionam em alguns casos — o Node tenta detectar exports nomeados de forma estática —, mas, se você quer garantia, importe o default e use desestruturação:

import pkg from "./utils.cjs";
const { parse, stringify } = pkg;

CommonJS importando ESM é o caminho doloroso. Não dá pra usar require() em um módulo ES — isso dispara ERR_REQUIRE_ESM. A saída é recorrer ao import() dinâmico, que também funciona em CJS e devolve uma Promise:

index.js
Output
Click Run to see the output here.

O Node moderno (versão 22+) incluiu um require() síncrono para ESM em certas condições, mas o import() dinâmico continua sendo a resposta portátil.

Outras diferenças de comportamento que vale conhecer

Além da sintaxe, os dois sistemas divergem em alguns detalhes que, de vez em quando, pegam a gente de surpresa:

index.js
Output
Click Run to see the output here.

Mais algumas diferenças:

  • this no escopo global do arquivo. No CJS, this aponta para module.exports. Já no ESM, é undefined — afinal, ESM roda sempre em modo estrito.
  • __dirname e __filename. No CJS, você já tem essas variáveis prontas. No ESM, precisa obtê-las a partir de import.meta.url:
import { fileURLToPath } from "node:url";
import { dirname } from "node:path";

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
  • Extensões de arquivo nos imports. No ESM, você precisa incluir a extensão ("./utils.js", e não "./utils") em caminhos relativos. Já o CJS é mais tolerante.
  • Live bindings vs. snapshots. Os imports do ESM são referências vivas para as variáveis do módulo que exportou. O CJS entrega uma cópia do que foi atribuído a module.exports no momento do carregamento. Na prática a maior parte do código nem percebe a diferença, mas isso importa em dependências circulares.

Qual dos dois usar?

Para um projeto novo: ES Modules. Coloque "type": "module" no package.json e siga em frente. O ESM é o padrão da linguagem, funciona do mesmo jeito no navegador e no Node, suporta await no top-level e as ferramentas modernas já nascem pensadas para ele.

Fique no CommonJS quando:

  • Você mantém uma base de código CJS já existente e a migração ainda não compensa.
  • Você publica uma biblioteca que precisa dar suporte a versões bem antigas do Node ou a consumidores que não conseguem usar ESM.
  • Uma dependência crítica só entrega CJS e a interoperabilidade dela é bagunçada. (Raro hoje em dia, mas acontece.)

Mesmo nesses casos, você vai ler código ESM o tempo todo — praticamente tudo que foi publicado no npm nos últimos anos vai nessa direção. Ser fluente nos dois não é opcional; ser fluente nos idiomas daquele que você está realmente escrevendo também não.

Um checklist mental rápido

Ao abrir um arquivo novo, se pergunte:

  • Esse arquivo usa import/export ou require/module.exports? Não misture os dois.
  • O que o package.json mais próximo diz sobre o "type"?
  • Se você está importando um pacote, dá uma olhada no package.json dele — ele entrega ESM, CJS ou os dois?
  • Se apareceu um ERR_REQUIRE_ESM, você está em CJS tentando carregar ESM. Troque por import() dinâmico ou migre quem está chamando para ESM.

Noventa por cento da confusão com módulos no Node cai em um desses quatro pontos.

A seguir: o básico de npm

Módulos servem para dividir o seu código em vários arquivos. O próximo passo é trazer para o projeto código escrito por outras pessoas — e é para isso que existe o npm. Vamos ver como instalar pacotes, entender as faixas de versão do semver e as partes do fluxo do npm que você realmente usa no dia a dia.

Perguntas frequentes

Qual a diferença entre require e import no JavaScript?

O require é a forma do CommonJS de carregar módulos: é síncrono, executa na hora em que é chamado e devolve o que o módulo atribuiu a module.exports. Já o import é a sintaxe de ES Modules — é estático, sobe pro topo do arquivo (hoisting) e é analisado antes do código rodar. Os dois também divergem em coisas como o valor de this, o jeito que dependências circulares são resolvidas e se dá pra usar await no topo do arquivo.

Devo usar CommonJS ou ES Modules em um projeto Node novo?

Vai de ES Modules. Coloca "type": "module" no package.json e usa import/export normalmente. ESM é o padrão oficial, funciona tanto no navegador quanto no Node e ainda suporta top-level await. O CommonJS continua aparecendo em pacotes antigos e em algumas ferramentas, então você vai ler bastante CJS mesmo sem escrever.

Dá pra misturar require e import no mesmo projeto?

Dá, mas tem regra. Arquivo .mjs ou pacote com "type": "module" roda como ESM; .cjs ou "type": "commonjs" roda como CJS. Um módulo ESM consegue dar import em um módulo CommonJS (o module.exports aparece como default export). Já o contrário não rola direto: CommonJS não consegue usar require() em um módulo ESM — tem que usar import() dinâmico, que devolve uma Promise.

Aprenda a programar com o Coddy

COMEÇAR