Pular para o conteúdo
Cursos

DevClub

LógicaFront-endBack-endMobile

IA Club

IA na prática
Estudar programaçãoEstudar IA
LiçãoIniciantecódigo testado

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.

Rodolfo Mori11 min de leitura

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:

bash
mkdir livraria && cd livraria
npm init -y
Wrote to /private/tmp/livraria/package.json:

{ “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:

bash
npm install express
added 68 packages, and audited 69 packages in 1s

27 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:

bash
npm install --save-dev prettier
added 1 package, and audited 70 packages in 636ms

28 packages are looking for funding run npm fund for details

found 0 vulnerabilities

Os dois foram parar em listas diferentes do package.json:

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:

bash
rm -rf node_modules && npm ci && du -sh node_modules
rm -rf node_modules && npm ci --omit=dev && du -sh node_modules
added 69 packages, and audited 70 packages in 768ms 13M node_modules

added 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:

bash
npm install semver
npm view express versions --json > versoes.json
js
const 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));
}
4.18.2 1 versões, maior: 4.18.2 ~4.18.2 2 versões, maior: 4.18.3 ^4.18.2 12 versões, maior: 4.22.2

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:

bash
npm install --save-exact express@4.22.2
npm install --save-prefix="~" cors
{ "cors": "~2.8.6", "express": "4.22.2" }

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

bash
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))"
{ "version": "5.2.1", "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==" }

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:

js
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:

npm install media 730 ms (min 665 / max 817), 5 execucoes npm ci media 770 ms (min 565 / max 954), 5 execucoes

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:

npm install media 974 ms (min 828 / max 1219), 5 execucoes npm ci media 1461 ms (min 990 / max 1852), 5 execucoes

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:

bash
npm ci
npm error code EUSAGE npm error npm error `npm ci` can only install packages when your package.json and package-lock.json or npm-shrinkwrap.json are in sync. Please update your lock file with `npm install` before continuing. npm error npm error Missing: zod@4.4.3 from lock file

Algué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:

npm error code EUSAGE npm error npm error The `npm ci` command can only install with an existing package-lock.json or npm error npm-shrinkwrap.json with lockfileVersion >= 1. Run an install with npm@5 or npm error later to generate a package-lock.json file, then try again.

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:

bash
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:

js
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);
}
js
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:

bash
npm run
Lifecycle scripts included in livraria@1.0.0: test node --test start node src/servidor.js available via `npm run`: dev node --watch src/servidor.js format prettier --write .

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

bash
npm test
> livraria@1.0.0 test > node --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:

bash
npm run dev
> livraria@1.0.0 dev > node --watch src/servidor.js

Livraria 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:

bash
npm start
curl -s http://localhost:4488/livros/disponiveis
> livraria@1.0.0 start > node src/servidor.js

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

bash
npx prettier --version
npx --yes semver 4.22.2 -r "^4.18.2"
npx --yes semver 5.2.1 -r "^4.18.2"
3.9.6

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:

bash
npm ls
livraria@1.0.0 /private/tmp/livraria ├── express@5.2.1 └── prettier@3.9.6

A segunda responde a pergunta que aparece quando um alerta de segurança cita um pacote que você nunca instalou:

bash
npm explain cookie
cookie@0.7.2 node_modules/cookie cookie@"^0.7.1" from express@5.2.1 node_modules/express express@"^5.2.1" from the root project

Leia 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:

bash
npm outdated
Package Current Wanted Latest Location Depended by express 4.18.2 4.22.2 5.2.1 node_modules/express livraria-legado

Current é 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:

bash
npm audit
7 vulnerabilities (3 low, 1 moderate, 3 high)

To 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:

bash
npm update
changed 14 packages, and audited 69 packages in 863ms

15 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:

bash
diff -u lock-antes.json package-lock.json | wc -l
335

O trecho do Express dentro desse diff:

"node_modules/express": { - "version": "4.18.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", - "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz", + "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==", "license": "MIT",

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:

json
{
  "scripts": {
    "dev": "node --watch src/servidor.js",
    "start": "node src/servidor.js",
  }
}
npm error code EJSONPARSE npm error JSON.parse Invalid package.json: JSONParseError: Expected double-quoted property name in JSON at position 163 (line 8 column 3) while parsing near "...src/servidor.js\",\n },\n \"dependencies\":..." npm error JSON.parse Failed to parse JSON data. npm error JSON.parse Note: package.json must be actual JSON, not just JavaScript.

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:

bash
npm run star
npm error Missing script: "star" npm error npm error Did you mean one of these? npm error npm stars # View packages marked as favorites npm error npm start # Start a package npm error npm error To see a list of scripts, run: npm error npm run

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

  • npm
  • package.json
  • semver
  • scripts
  • node

Perguntas frequentes

Preciso instalar o npm separado do Node?
Não. O npm vem junto com o instalador do Node e com qualquer versão baixada pelo nvm. Cada versão do Node traz a sua, e é por isso que trocar de versão de Node troca também a versão de npm que responde no terminal.
Qual a diferença entre instalar um pacote no projeto e instalar com -g?
Sem -g o pacote entra no node_modules do projeto e fica registrado no package.json, então quem clonar o repositório recebe a mesma coisa. Com -g ele é instalado uma vez na máquina, some do package.json e não acompanha o projeto. Ferramenta de linha de comando raramente precisa de -g hoje, porque o npx resolve.
Dá para usar yarn ou pnpm no lugar do npm?
Dá, e os três leem o mesmo package.json. O que muda é o arquivo de trava: yarn.lock e pnpm-lock.yaml no lugar do package-lock.json. Escolha um por projeto e escreva isso no README, porque dois arquivos de trava no mesmo repositório é receita de instalações diferentes entre as máquinas.
Para que serve o campo engines no package.json?
Ele declara em que versões de Node o projeto foi feito para rodar, por exemplo "node&nbsp;&gt;=&nbsp;20". Por padrão o npm só avisa, mas plataformas de deploy leem esse campo para escolher a imagem, e um npm config set engine-strict true transforma o aviso em erro.

Dúvidas e comentários

Travou em algum passo? Pergunte aqui — a equipe e outros alunos respondem.

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

  1. npm Docs — package.json — docs.npmjs.com
  2. npm Docs — npm ci — docs.npmjs.com
  3. Semantic Versioning 2.0.0 — semver.org
  4. Node.js — Watch mode — nodejs.org

Continue por aqui