Error: Cannot find module no Node.js: causas e correção
As seis causas do Cannot find module no Node, do npm install esquecido ao caminho sem extensão em ESM, cada uma reproduzida com o stack trace real.
Cannot find module significa que o resolvedor de módulos do Node tentou
transformar um nome de importação em um arquivo e não encontrou um destino
válido. A correção depende de o que estava sendo resolvido: um pacote do npm
ou um caminho do seu próprio projeto. O campo code, o nome procurado e a pilha
de importações entregam essa diferença.
Pense numa encomenda com endereço incompleto. Se o destinatário é express, o
Node procura o pacote nas pastas node_modules permitidas. Se o endereço começa
com ./, ele parte da pasta do arquivo que importou e segue o caminho relativo.
Nome errado, extensão ausente ou pacote não instalado levam a buscas diferentes,
mas terminam no mesmo aviso: não houve arquivo para carregar. É por isso que o
primeiro passo é ler o endereço do erro, não reinstalar tudo no escuro.
Este artigo reproduz seis causas frequentes num projeto real rodando no Node 24.16.0 e mostra como identificar qual busca falhou.
O projeto é a API da biblioteca do bairro: acervo, empréstimos e um servidor
Express. Ele vive em /private/tmp/biblioteca, e é dele que sai cada stack
trace daqui para baixo.
Leia o código do erro antes de tocar no projeto
O Node tem dois carregadores de módulo, e eles falham com mensagens diferentes.
Este é o mesmo import express faltando, escrito nos dois formatos. Primeiro em
CommonJS, no arquivo src/servidor.cjs:
const express = require('express');
const app = express();
app.get('/livros', (req, res) => res.json([{ titulo: 'Dom Casmurro' }]));
app.listen(4477);Error: Cannot find module ‘express’ Require stack:
- /private/tmp/biblioteca/src/servidor.cjs at Module._resolveFilename (node:internal/modules/cjs/loader:1500:15) at wrapResolveFilename (node:internal/modules/cjs/loader:1071:27) at defaultResolveImplForCJSLoading (node:internal/modules/cjs/loader:1095:10) at resolveForCJSWithHooks (node:internal/modules/cjs/loader:1116:12) at Module._load (node:internal/modules/cjs/loader:1285:25) at wrapModuleLoad (node:internal/modules/cjs/loader:255:19) at Module.require (node:internal/modules/cjs/loader:1600:12) at require (node:internal/modules/helpers:153:16) at Object.<anonymous> (/private/tmp/biblioteca/src/servidor.cjs:1:17) at Module._compile (node:internal/modules/cjs/loader:1854:14) { code: ‘MODULE_NOT_FOUND’, requireStack: [ ‘/private/tmp/biblioteca/src/servidor.cjs’ ] }
Node.js v24.16.0
Agora o mesmo pacote faltando, no arquivo ESM src/servidor.js:
import express from 'express';
const app = express();
app.get('/livros', (req, res) => res.json([{ titulo: 'Dom Casmurro' }]));
app.listen(4477);Error [ERR_MODULE_NOT_FOUND]: Cannot find package ‘express’ imported from /private/tmp/biblioteca/src/servidor.js at Object.getPackageJSONURL (node:internal/modules/package_json_reader:301:9) at packageResolve (node:internal/modules/esm/resolve:768:81) at moduleResolve (node:internal/modules/esm/resolve:859:18)
Repare na diferença que vale ouro: o ESM escreveu package, não module. Ele só usa a palavra “package” quando o que faltou é um pacote do npm. Quando o que faltou é um arquivo seu, a mensagem muda para “module” e vem com o caminho absoluto exato que ele tentou abrir.
| a mensagem diz | o Node estava procurando | por onde começar |
|---|---|---|
MODULE_NOT_FOUND + Require stack |
pacote, nas pastas node_modules subindo a árvore |
ler a requireStack de baixo para cima: o último arquivo é quem pediu |
ERR_MODULE_NOT_FOUND · Cannot find package |
pacote, do mesmo jeito | package.json e npm ci |
ERR_MODULE_NOT_FOUND · Cannot find module /caminho |
aquele arquivo, e só ele | comparar o caminho impresso com o que existe no disco |
A diferença entre os dois carregadores está explicada em
ESM ou CommonJS no Node. Para o resto deste
artigo, o projeto da biblioteca está com "type": "module" no package.json.
Causa 1: o pacote nunca foi instalado nesta máquina
É o caso mais comum e o mais rápido: você clonou o repositório e rodou o
servidor antes de instalar as dependências. O package.json lista o Express, a
pasta node_modules não existe.
npm ci
node src/servidor.jsUm detalhe que engana muita gente: node_modules existir não quer dizer que
o pacote certo está lá dentro. O Node procura subindo a árvore de pastas, então
uma node_modules de um projeto vizinho, ou da sua pasta pessoal, pode fazer o
import funcionar hoje e quebrar amanhã em outra máquina. Quem manda é esta lista:
console.log(module.paths.join('\n'));Causa 2: em ESM o caminho relativo precisa da extensão .js
Aqui o Node acha o pacote, mas não acha o seu arquivo. O servidor.js
importa a rota de livros sem escrever .js no fim:
import express from 'express';
import { listarLivros } from './rotas/livros';
const app = express();
app.get('/livros', listarLivros);
app.listen(4477, () => console.log('Biblioteca no ar em http://localhost:4477'));O campo url entrega o diagnóstico inteiro: o Node procurou um arquivo chamado
livros, sem extensão nenhuma, e é isso que ele quis dizer literalmente. Em
módulos com import e export, o
especificador relativo é uma URL — e URL não adivinha extensão. CommonJS
adivinha, e é por isso que o mesmo código funciona em .cjs:
const { acervo } = require('./rotas/livros');
console.log(`CommonJS achou sem extensão: ${acervo.length} livros`);A correção é escrever './rotas/livros.js'. Nada de flag: a
--experimental-specifier-resolution=node, que aparece em resposta antiga de
fórum, é aceita pelo Node 24 sem erro e simplesmente não faz mais efeito
nenhum — testei, e o import continua quebrando igual.
Causa 3: maiúscula no nome do arquivo passa no Mac e quebra no Linux
Este é o erro que ninguém reproduz no próprio computador. O arquivo no disco se
chama livros.js, e o import escreveu Livros.js:
import { acervo } from './rotas/Livros.js';
console.log(`Acervo carregado: ${acervo.length} livros`);No macOS com APFS padrão, isso roda:
Para provar o que acontece no servidor, criei um volume APFS case-sensitive
com hdiutil, copiei o projeto inteiro para lá e rodei o mesmo arquivo:
diskutil info /Volumes/CaseSens | grep "File System Personality"
node src/caso.jsMesmo projeto, mesmo Node, mesma linha de código. Só mudou o sistema de arquivos. É exatamente isso que acontece quando o deploy roda numa imagem Linux e o seu Mac dizia que estava tudo bem.
Como o Git costuma ignorar a troca de caixa, vale ter um verificador no
package.json que compara cada import relativo com o nome real no disco:
import { readdir, readFile } from 'node:fs/promises';
import path from 'node:path';
const raiz = path.join(import.meta.dirname, '..', 'src');
const importRelativo = /from\s+['"](\.[^'"]+)['"]/g;
async function listarArquivos(pasta) {
const entradas = await readdir(pasta, { withFileTypes: true });
const saida = [];
for (const entrada of entradas) {
const caminho = path.join(pasta, entrada.name);
if (entrada.isDirectory()) saida.push(...(await listarArquivos(caminho)));
else if (caminho.endsWith('.js')) saida.push(caminho);
}
return saida;
}
let problemas = 0;
for (const arquivo of await listarArquivos(raiz)) {
const codigo = await readFile(arquivo, 'utf8');
for (const [, especificador] of codigo.matchAll(importRelativo)) {
const alvo = path.resolve(path.dirname(arquivo), especificador);
const vizinhos = await readdir(path.dirname(alvo));
if (!vizinhos.includes(path.basename(alvo))) {
problemas++;
console.log(`caixa errada: ${path.relative(raiz, arquivo)} importa "${especificador}"`);
}
}
}
process.exit(problemas === 0 ? 0 : 1);Ele funciona porque readdir devolve o nome real gravado no disco, mesmo
num sistema que não diferencia caixa na hora de abrir. Depois de corrigir o
import para livros.js, o script sai calado e com código 0.
Causa 4: node_modules velho depois de trocar de branch
Você trocou de branch, o colega adicionou o Zod para validar o empréstimo, e o
seu node_modules continua sendo o de ontem:
import { z } from 'zod';
export const esquemaEmprestimo = z.object({
livroId: z.number().int().positive(),
leitor: z.string().min(3),
});Um comando confirma o desencontro entre o que o package.json promete e o que
está instalado, e ele sai com código 1 — dá para pôr no CI:
npm ls --depth=0npm error code ELSPROBLEMS npm error missing: zod@^4.1.12, required by biblioteca@1.0.0
O contrário também acontece, e é pior porque não dá erro nenhum na sua máquina.
Alguém instalou um pacote sem salvar no package.json, o código passou a usar,
e tudo funciona local:
npm install --no-save nanoid
node -e "import('nanoid').then(m => console.log('codigo do emprestimo:', m.nanoid(8)))"
npm ls --depth=0extraneous é o aviso de que aquele pacote está no disco e não está no
contrato. O npm ci apaga a node_modules e reinstala só o que o
package-lock.json manda — e o mesmo código morre:
npm ci
node -e "import('nanoid')"Esse é o “funciona na minha máquina” em estado puro. Se você quer descobrir o
problema antes do CI, use npm ci também no seu computador quando trocar de
branch — ele é o comando que respeita o
package.json e o lock ao pé da letra.
Causa 5: dependência de dev num deploy que rodou –omit=dev
O src/config.js da biblioteca lia o .env com o pacote dotenv:
import 'dotenv/config';
export const config = {
porta: Number(process.env.PORTA ?? 4477),
acervoMaximo: Number(process.env.ACERVO_MAXIMO ?? 5000),
};Local funciona, porque npm install instala tudo. O deploy roda o comando que
economiza imagem, e aí o dotenv não vem junto:
npm ci --omit=dev
node -e "import('./src/config.js')"A regra é simples: se o código que roda em produção importa o pacote, ele é
dependencies. devDependencies é para o que só existe na sua máquina —
linter, ferramenta de teste, nodemon.
Neste caso específico há uma saída melhor ainda, que é apagar a dependência: o
Node lê arquivo .env sozinho desde a versão 20.6, com --env-file. O
config.js fica sem nenhum import:
export const config = {
porta: Number(process.env.PORTA ?? 4477),
acervoMaximo: Number(process.env.ACERVO_MAXIMO ?? 5000),
};node --env-file=.env -e "import('./src/config.js').then(m => console.log(m.config))"
node -e "import('./src/config.js').then(m => console.log(m.config))"A segunda linha é o mesmo arquivo sem a flag: sem .env carregado, os valores
padrão entram. Tem mais sobre isso em
variáveis de ambiente no Node.
Causa 6: caminho montado com process.cwd() em vez de import.meta.dirname
A biblioteca registra as rotas com import() dinâmico, montando o caminho a
partir da pasta onde o comando foi digitado:
import path from 'node:path';
const arquivosDeRota = ['livros.js', 'emprestimos.js'];
export async function carregarRotas() {
const carregadas = [];
for (const arquivo of arquivosDeRota) {
const caminho = path.join(process.cwd(), 'src', 'rotas', arquivo);
await import(caminho);
carregadas.push(arquivo);
}
return carregadas;
}Da raiz do projeto, funciona. De dentro de src/, não:
O src/src no meio do caminho é a resposta inteira. process.cwd() é a pasta
do terminal, não a pasta do arquivo — e ela muda quando o systemd, o Docker ou
um script npm sobe o processo de outro lugar. Trocando por import.meta.dirname,
que é a pasta do próprio módulo, o caminho para de depender de onde você está:
const caminho = path.join(import.meta.dirname, 'rotas', arquivo);São três execuções do mesmo arquivo, da raiz do projeto, de dentro de src/ e
de /Users. Se você tentar usar __dirname num arquivo ESM, o erro é outro e a
mensagem já entrega a correção:
Roteiro de diagnóstico em três comandos
Quando nenhuma das seis causas saltar aos olhos, pare de chutar e pergunte ao
Node onde ele procurou. A variável NODE_DEBUG=module imprime a lista completa
de pastas antes de falhar:
NODE_DEBUG=module node -e "require('dotenv')" 2>&1 | grep dotenvSe o pacote está instalado, o segundo comando mostra qual arquivo o Node vai abrir de fato — útil quando existe mais de uma cópia da mesma biblioteca no monorepo:
node -e "console.log(require.resolve('express'))"
node --input-type=module -e "console.log(import.meta.resolve('./src/rotas/livros.js'))"E o terceiro é o npm ls --depth=0 da causa 4, que compara o prometido com o
instalado. Na ordem: NODE_DEBUG diz onde ele procurou, resolve diz o que ele
achou, npm ls diz o que deveria estar lá.
O próximo passo
Com o servidor subindo de novo, vale fechar o buraco que deixou o erro entrar:
rode npm ci sempre que trocar de branch, e ponha npm ls --depth=0 no seu
pipeline. Se o erro apareceu montando a API, a
primeira rota no Express mostra a estrutura
de pastas que evita import relativo comprido, e o
deploy de API Node trata do --omit=dev e do
Dockerfile. O caminho completo, do zero à API no ar, está no
guia de Node.js.
Antes de encerrar, rode o roteiro no seu projeto: npm ls --depth=0,
require.resolve ou import.meta.resolve para o módulo suspeito e, se ainda
falhar, NODE_DEBUG=module. Anote o nome exato procurado e a primeira pasta em
que ele deveria existir. A prática está concluída quando você consegue apontar
uma causa concreta — dependência ausente, caminho incorreto ou diferença entre
ambientes — sem apagar node_modules por tentativa.
Perguntas frequentes
Instalei o pacote com npm install -g e o erro continua. Por quê?
npm root -g com o que node -e "console.log(module.paths)" imprime — a global termina em lib/node_modules e a lista do Node tem só lib/node. Pacote que o seu código importa vai como dependência do projeto; global é para CLI que você digita no terminal.Existe alguma flag para o ESM voltar a achar arquivo sem extensão?
--experimental-specifier-resolution=node que aparece em resposta antiga de fórum ainda é aceita pelo Node 24 sem reclamar, e não faz efeito nenhum — o mesmo import continua falhando. Escreva a extensão ou declare um imports no package.json.Importei um .json e o Node reclamou. É o mesmo erro?
TypeError [ERR_IMPORT_ATTRIBUTE_MISSING], dizendo que o módulo precisa de type: json. O arquivo foi encontrado; o que faltou foi import acervo from './dados/acervo.json' with { type: 'json' }.O erro só acontece dentro do Docker. O que investigar primeiro?
npm ci --omit=dev enquanto algum import de produção aponta para pacote que está em devDependencies.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
- Node.js — Modules: ECMAScript modules — nodejs.org
- Node.js — Modules: CommonJS modules — nodejs.org
- npm Docs — npm ci — docs.npmjs.com


