npm e package.json: instalar pacote, semver e scripts
O que cada campo do package.json faz, a diferença entre ^ e ~, para que serve o package-lock e como criar scripts que o time inteiro usa.
O package.json é a carteira de identidade do seu projeto Node: ele diz o nome,
quais pacotes o código precisa para funcionar e quais comandos o time roda. O
npm é o programa que lê esse arquivo, baixa o que falta e executa esses comandos.
O projeto deste artigo é a API do catálogo de uma livraria de bairro. Tudo aqui foi executado num MacBook com chip M4 Pro, Node 24.16.0 e npm 11.13.0 — os tempos e as saídas são dessa máquina. Se você ainda não tem o Node instalado, comece por instalar o Node com o nvm.
A ficha e o almoxarifado: como npm e package.json se dividem
Pense no package.json como a ficha de uma obra: ele registra o nome do projeto,
os materiais necessários e os comandos que a equipe combinou. O npm é o
almoxarife que lê essa ficha, busca as caixas certas e entrega cada ferramenta
quando você pede. A ficha não baixa nada sozinha, e o almoxarife não adivinha o
que o projeto precisa.
No mecanismo real, package.json é o manifesto e npm é o gerenciador de
pacotes. dependencies descreve necessidades de execução, scripts nomeia
comandos e o lockfile fixa as versões resolvidas. Ao rodar o primeiro comando,
compare o arquivo antes e depois: a mudança escrita no manifesto é a prova de
qual decisão o npm acabou de registrar.
Criar o package.json na mão dá certo, mas ninguém faz isso. O comando
npm init -y escreve um esqueleto válido em milissegundos:
mkdir livraria && cd livraria
npm init -y{ “name”: “livraria”, “version”: “1.0.0”, “description”: “”, “main”: “index.js”, “scripts”: { “test”: “echo "Error: no test specified" && exit 1” }, “keywords”: [], “author”: “”, “license”: “ISC”, “type”: “commonjs” }
Cinco campos importam agora. name e version identificam o pacote — se um dia
você publicar no npm, é esse par que vira o endereço. main diz qual arquivo é
a porta de entrada do projeto. scripts guarda os atalhos de terminal. E type
decide se os seus arquivos usam import ou require; o padrão é commonjs, e
a diferença está em ESM ou CommonJS no Node.
dependencies e devDependencies: o que sobe para o servidor
Instalar um pacote é um comando só. Repare no número que volta:
npm install express27 packages are looking for funding
run npm fund for details
found 0 vulnerabilities
Você pediu um pacote e chegaram 68. Os outros 67 são as dependências do Express,
e as dependências delas. Isso é normal no ecossistema Node, e é o motivo de
node_modules nunca ir para o Git.
Agora um pacote que só serve enquanto você programa — um formatador de código:
npm install --save-dev prettier28 packages are looking for funding
run npm fund for details
found 0 vulnerabilities
Os dois foram parar em listas diferentes do package.json:
{
"dependencies": {
"express": "^5.2.1"
},
"devDependencies": {
"prettier": "^3.9.6"
}
}A separação não é organizacional, é operacional. No servidor você instala com
--omit=dev e o Prettier simplesmente não é baixado:
rm -rf node_modules && npm ci && du -sh node_modules
rm -rf node_modules && npm ci --omit=dev && du -sh node_modulesadded 68 packages, and audited 69 packages in 575ms 3.8M node_modules
Um pacote a menos na conta, 9,2 MB a menos no disco. Num projeto real, com
TypeScript e uma suíte de testes no devDependencies, essa diferença passa
fácil de 100 MB — e ela entra em cada imagem de container que você publica.
A regra prática: se o código que roda em produção faz import do pacote, ele é
dependencies. Se o pacote só é chamado por um script seu (testar, formatar,
compilar), ele é devDependencies.
O acento circunflexo: o que ^ libera e o que ~ segura
Repare que o npm não escreveu 5.2.1, escreveu ^5.2.1. Esse símbolo é uma
faixa de versões aceitáveis, e ele segue o versionamento semântico: em
MAIOR.MENOR.CORREÇÃO, o primeiro número muda quando algo quebra, o segundo
quando algo é adicionado, o terceiro quando algo é corrigido.
Dá para medir exatamente o que cada faixa libera. Baixei a lista real de versões publicadas do Express e testei três faixas contra ela:
npm install semver
npm view express versions --json > versoes.jsonconst semver = require('semver');
const versoes = require('./versoes.json').filter((v) => !v.includes('-'));
for (const faixa of ['4.18.2', '~4.18.2', '^4.18.2']) {
const aceitas = versoes.filter((v) => semver.satisfies(v, faixa));
console.log(faixa.padEnd(8), aceitas.length, 'versões, maior:', semver.maxSatisfying(versoes, faixa));
}Fixar 4.18.2 aceita uma única versão. O ~ liberou duas — só correções. O ^
liberou doze, indo até a 4.22.2, dois anos de novidades adiante. E nenhuma
delas passa para a 5, porque o ^ nunca cruza a fronteira do primeiro número.
| você escreve | o npm aceita | quando usar |
|---|---|---|
^5.2.1 |
5.2.1 até antes da 6.0.0 | padrão, para quase tudo |
~5.2.1 |
5.2.1 até antes da 5.3.0 | pacote que já te queimou numa versão menor |
5.2.1 |
só a 5.2.1 | dependência crítica, ou reprodução de bug |
Para escolher outro comportamento na hora de instalar, o npm tem duas flags:
npm install --save-exact express@4.22.2
npm install --save-prefix="~" corspackage-lock.json: a fotografia do que foi instalado de verdade
O package.json diz “qualquer 5.x a partir da 5.2.1”. Isso é uma faixa, não uma
resposta — e duas pessoas instalando em dias diferentes podem receber árvores
diferentes. O package-lock.json fecha essa porta: ele registra a versão exata,
a URL do pacote e um hash de integridade de cada pacote da árvore.
node -e "const e = require('./package-lock.json').packages['node_modules/express']; console.log(JSON.stringify({ version: e.version, resolved: e.resolved, integrity: e.integrity }, null, 2))"São 69 entradas como essa no lock desta livraria, uma para cada pacote da árvore. É por isso que ele é grande, feio de ler e precisa ir para o Git. Sem ele, o “na minha máquina funciona” volta a existir.
npm install ou npm ci: a diferença aparece no deploy
A crença comum é que npm ci existe porque é mais rápido. Eu medi. O script
apaga o node_modules, roda o comando, cronometra, repete seis vezes e descarta
a primeira execução como aquecimento:
import { execSync } from 'node:child_process';
import { rmSync } from 'node:fs';
const dir = process.argv[2];
function medir(comando) {
const tempos = [];
for (let i = 0; i < 6; i++) {
rmSync(`${dir}/node_modules`, { recursive: true, force: true });
const t0 = performance.now();
execSync(comando, { cwd: dir, stdio: 'ignore' });
tempos.push(performance.now() - t0);
}
tempos.shift();
return tempos.reduce((s, t) => s + t, 0) / tempos.length;
}
for (const cmd of ['npm install', 'npm ci']) {
console.log(cmd.padEnd(12), medir(cmd).toFixed(0), 'ms');
}Na livraria enxuta, com 69 pacotes no lock:
E na mesma livraria com a pilha completa da API — Express, CORS, Zod, bcrypt, JWT e mais os pacotes de desenvolvimento, 206 pacotes no lock:
Com o cache do npm quente, npm ci foi mais lento nas duas medições. Então
não é velocidade. O que o npm ci entrega é outra coisa: ele se recusa a
adivinhar. Se o package.json e o lock discordarem, ele para:
npm ciAlguém adicionou zod ao package.json e commitou sem o lock atualizado. O
npm install teria resolvido sozinho e seguido em frente com uma árvore que
ninguém revisou; o npm ci derruba o pipeline e mostra o culpado pelo nome. É
exatamente isso que você quer num deploy de API.
O outro recado do npm ci aparece em projeto que ainda não tem lock nenhum:
Resumindo: npm install na sua máquina, quando você está mexendo nas
dependências. npm ci no servidor e no CI, sempre.
Scripts: os atalhos que o time inteiro compartilha
O campo scripts transforma comando comprido em nome curto. Dá para editá-lo no
arquivo, mas o npm pkg set faz isso sem risco de vírgula errada:
npm pkg set type=module main=src/servidor.js
npm pkg set scripts.dev="node --watch src/servidor.js"
npm pkg set scripts.start="node src/servidor.js"
npm pkg set scripts.test="node --test"
npm pkg set scripts.format="prettier --write ."O catálogo da livraria vive em dois arquivos pequenos:
export const livros = [
{ id: 1, titulo: 'Grande Sertão: Veredas', autor: 'Guimarães Rosa', preco: 79.9, estoque: 4 },
{ id: 2, titulo: 'Vidas Secas', autor: 'Graciliano Ramos', preco: 42.5, estoque: 0 },
{ id: 3, titulo: 'A Hora da Estrela', autor: 'Clarice Lispector', preco: 38.0, estoque: 7 },
];
export function disponiveis(lista = livros) {
return lista.filter((livro) => livro.estoque > 0);
}import express from 'express';
import { livros, disponiveis } from './catalogo.js';
const app = express();
app.get('/livros', (req, res) => res.json(livros));
app.get('/livros/disponiveis', (req, res) => res.json(disponiveis()));
app.listen(4488, () => console.log('Livraria no ar em http://localhost:4488'));Digitar npm run sem nada lista o que existe — é o primeiro comando a rodar
quando você cai num projeto que não é seu:
npm runRepare que test e start aparecem separados, sob “Lifecycle scripts”. São
nomes que o npm reconhece: você chama npm test e npm start, sem o run.
Qualquer outro nome precisa do npm run na frente.
npm test✔ esconde o livro sem estoque (1.035083ms) ℹ tests 1 ℹ suites 0 ℹ pass 1 ℹ fail 0 ℹ duration_ms 83.550041
O teste é o do próprio Node, sem instalar nada — o assunto tem uma lição só para ele em testar rota de API com node:test.
O npm run dev é o script que você mais vai usar. O --watch é nativo do Node
desde a versão 18, e substitui o nodemon que ainda aparece em tutorial antigo.
Aqui eu subi o servidor e mudei o estoque de um livro no catalogo.js:
npm run devLivraria no ar em http://localhost:4488 Restarting ‘src/servidor.js’ Livraria no ar em http://localhost:4488
E o npm start, com a API respondendo de verdade:
npm start
curl -s http://localhost:4488/livros/disponiveisLivraria no ar em http://localhost:4488 [{“id”:1,“titulo”:“Grande Sertão: Veredas”,“autor”:“Guimarães Rosa”,“preco”:79.9,“estoque”:4},{“id”:3,“titulo”:“A Hora da Estrela”,“autor”:“Clarice Lispector”,“preco”:38,“estoque”:7}]
“Vidas Secas” ficou de fora porque o estoque é zero. Se essa parte do Express ainda é nebulosa, ela é destrinchada em Express do zero: a primeira rota.
npx: rodar uma ferramenta sem instalar
O npx vem junto com o npm e resolve um binário em duas etapas: primeiro
procura em node_modules/.bin do projeto, e só se não achar baixa uma cópia
temporária do registro.
npx prettier --version
npx --yes semver 4.22.2 -r "^4.18.2"
npx --yes semver 5.2.1 -r "^4.18.2"4.22.2
A primeira linha veio do Prettier que está no devDependencies — nada foi
baixado. A segunda usou o semver, que não está instalado neste projeto:
o npx buscou, rodou e confirmou que a 4.22.2 satisfaz ^4.18.2. A terceira não
imprimiu nada, porque a 5.2.1 está fora da faixa.
Isso importa por um motivo prático: quando você roda npx prettier dentro do
projeto, roda a versão que o package.json fixou — a mesma do colega ao lado.
Ferramenta instalada com -g não dá essa garantia.
Quem foi que trouxe esse pacote para cá
Duas ferramentas de leitura que quase ninguém usa no começo. A primeira mostra só o que você pediu, ignorando as 67 dependências transitivas:
npm lsA segunda responde a pergunta que aparece quando um alerta de segurança cita um pacote que você nunca instalou:
npm explain cookieLeia de baixo para cima: o projeto pediu Express, o Express pediu cookie na
faixa ^0.7.1, e o npm resolveu para a 0.7.2. Você não pode atualizar o
cookie sozinho — quem manda nele é o Express.
Atualizar sem quebrar o projeto numa sexta-feira
A livraria tem uma API antiga, ainda presa no Express 4.18.2. O npm outdated
mostra a situação em três colunas:
npm outdatedCurrent é o que está instalado. Wanted é o maior que a sua faixa ^
permite, e chegar até ele é seguro. Latest é o que existe no registro — o
salto para a 5 muda o número maior, logo tem quebra de compatibilidade e exige
ler o changelog.
Antes de mexer, vale ver o motivo da pressa:
npm auditTo address all issues, run: npm audit fix
Nenhuma delas está no seu código: são falhas em body-parser, cookie,
path-to-regexp, qs e send, todas puxadas pelo Express 4.18.2. O
npm update sobe até o limite do ^ e resolve as sete:
npm update15 packages are looking for funding
run npm fund for details
found 0 vulnerabilities
E aqui está a parte que quase ninguém percebe. Depois desse comando, o
package.json ficou byte a byte idêntico — o diff é vazio. O
package-lock.json mudou 335 linhas:
diff -u lock-antes.json package-lock.json | wc -lO trecho do Express dentro desse diff:
Ou seja: a atualização inteira mora no lock. Quem revisa só o package.json
no pull request não vê nada acontecendo — e quem apaga o lock por preguiça está
jogando fora a única evidência de que a árvore mudou.
Os dois erros que aparecem primeiro
O primeiro é uma vírgula. package.json é JSON puro, e JSON não aceita vírgula
sobrando antes de fechar a chave:
{
"scripts": {
"dev": "node --watch src/servidor.js",
"start": "node src/servidor.js",
}
}A última linha diz tudo: o arquivo parece JavaScript, mas não é. Além da vírgula final, ele não aceita comentário e não aceita aspas simples. O número da linha vem no erro — vá direto nela.
O segundo é um nome de script que não existe. O npm chuta o que você quis dizer:
npm run starUm terceiro erro é tão comum que ganhou lição própria: rodar o projeto sem ter
instalado nada e receber
Error: Cannot find module. A causa quase
sempre é o node_modules ausente, e a cura é um npm install.
O que vem depois
Com o package.json montado e os scripts no lugar, o próximo passo é escrever
código que o Node execute do jeito certo — e a primeira decisão é entre import
e require, no ESM ou CommonJS no Node. Para ver
onde cada assunto entra na sequência, o
guia completo de Node.js mostra o caminho até a API no ar.
Perguntas frequentes
Preciso instalar o npm separado do Node?
Qual a diferença entre instalar um pacote no projeto e instalar com -g?
Dá para usar yarn ou pnpm no lugar do npm?
Para que serve o campo engines no package.json?
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 com npm 11.13.0, e as saídas exibidas são as reais — como produzimos este conteúdo.
Fontes consultadas
- npm Docs — package.json — docs.npmjs.com
- npm Docs — npm ci — docs.npmjs.com
- Semantic Versioning 2.0.0 — semver.org
- Node.js — Watch mode — nodejs.org


