Menu

package.json: scripts, dependências e versões

Entenda o que vai dentro do package.json: os campos que importam, como os scripts funcionam e como o semver decide quais versões o npm instala.

O arquivo de manifesto de um projeto Node

Todo projeto Node.js tem um package.json na raiz. É um JSON comum que descreve o projeto — nome, versão, dependências, comandos disponíveis — e é esse arquivo que o npm lê sempre que precisa fazer qualquer coisa. Se você apagá-lo, o npm não faz a menor ideia do que é o seu projeto.

A forma mais rápida de criar um é rodando npm init:

npm init -y

A flag -y pula as perguntas e já aceita os valores padrão. No fim, você fica com algo parecido com isto:

{
  "name": "my-app",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

Esse é o esqueleto mínimo. A maioria desses campos não faz muita coisa sozinha — eles começam a fazer sentido conforme você vai adicionando dependências e scripts.

Dependencies vs devDependencies

Dois campos fazem praticamente todo o trabalho pesado do package.json: dependencies e devDependencies. Os dois funcionam como um mapa que associa o nome do pacote a uma faixa de versões.

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

A diferença entre os dois importa por um motivo: dependencies são pacotes que seu código precisa para rodar. Já devDependencies são pacotes que você só usa durante o desenvolvimento — ferramentas de teste, linters, bundlers, checadores de tipo. Quando alguém instala seu pacote como dependência de outro projeto, o npm baixa suas dependencies e ignora as devDependencies.

O npm atualiza esses campos automaticamente. Rodar npm install express adiciona uma linha em dependencies. Já npm install --save-dev vitest adiciona em devDependencies. Raramente você precisa mexer neles na mão.

Faixas de versão: ^, ~ e versão exata

Aquelas strings tipo ^4.19.0 não são versões exatas — são faixas. O npm segue o semver, que divide a versão em MAJOR.MINOR.PATCH:

  • MAJOR: mudanças que quebram compatibilidade.
  • MINOR: adiciona funcionalidades sem quebrar nada.
  • PATCH: correções de bugs.

Os dois operadores que você vai ver em todo lugar:

"express": "^4.19.0"   // >= 4.19.0 e < 5.0.0  (qualquer 4.x.x igual ou superior a 4.19.0)
"express": "~4.19.0"   // >= 4.19.0 e < 4.20.0 (qualquer 4.19.x igual ou superior a 4.19.0)
"express": "4.19.0"    // exatamente 4.19.0

^ é o padrão que o npm usa quando você instala um pacote. Ele confia que bumps de minor e patch vão manter a compatibilidade. Já o ~ é mais conservador — só aceita atualizações de patch. Uma versão sem prefixo fica travada exatamente naquela.

O detalhe traiçoeiro: "o que acabei de instalar" e "o que o range permite" não são a mesma coisa. Se você instalar express@4.19.0 hoje e um colega clonar o projeto daqui a um mês, o ^4.19.0 pode acabar resolvendo para 4.19.5. É aí que entra o package-lock.json — ele registra as versões exatas que foram resolvidas, garantindo que todo mundo tenha a mesma árvore de dependências. Commite esse arquivo.

Scripts do package.json: os atalhos do seu projeto

O campo scripts é onde você define atalhos para os comandos que mais usa no dia a dia. Qualquer coisa colocada ali pode ser executada com npm run <nome>:

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

Algumas coisas importantes sobre scripts:

  • npm start, npm test e alguns outros nomes específicos funcionam sem a palavra run. Todo o resto precisa de npm run <nome>.
  • Os scripts rodam em um shell com node_modules/.bin no PATH, então dá para chamar binários dos pacotes instalados direto. "test": "vitest" funciona mesmo sem o vitest estar instalado globalmente.
  • Dá para encadear scripts: "build": "npm run lint && npm run compile". Use && quando quiser "executar em sequência e parar se der erro".
  • Scripts com prefixo pre<nome> e post<nome> rodam automaticamente. Se você tiver um prebuild, ele roda antes do build sem precisar configurar mais nada.

Os scripts são a porta de entrada de comandos do projeto. Um bom package.json faz com que uma pessoa nova consiga clonar o repositório, rodar npm install e depois npm run dev / npm test sem precisar consultar nenhuma wiki.

Pontos de entrada: main, exports e type

Esses campos dizem ao Node (e aos bundlers) como carregar seu pacote.

index.js
Output
Click Run to see the output here.
  • type define como os arquivos .js são interpretados. "module" ativa ESM (import / export). Omita esse campo ou use "commonjs" para trabalhar com CommonJS (require). Para entender a fundo, dá uma olhada no doc sobre CommonJS vs ESM.
  • main é o entry point antigo — é o que require("my-lib") vai resolver. Ainda é respeitado por ferramentas mais velhas.
  • exports é o substituto moderno, bem mais rígido. Ele define exatamente quais arquivos podem ser importados de fora e em qual subpath. Se um arquivo não estiver listado ali, o import simplesmente falha — e isso é proposital, não um bug. Você controla a API pública do pacote.

Se você só está tocando uma aplicação (e não publicando um pacote), o type provavelmente é o único desses campos que vai te interessar.

Um package.json realista

Juntando tudo, é mais ou menos assim que fica o package.json de uma app Node pequena no dia a dia:

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

Repare no engines.node. Ele é apenas informativo — o npm emite um aviso (ou um erro, se você usar engine-strict) quando a versão do Node do usuário não bate. É uma boa prática em qualquer pacote que você for publicar.

Campos que vale a pena conhecer

Alguns outros campos que você vai encontrar por aí:

  • private: true — impede que você publique o pacote no npm sem querer. Ative isso em qualquer projeto que não seja pra publicação.
  • license — um identificador SPDX, como "MIT" ou "ISC". Faz diferença em projetos públicos.
  • repository, bugs, homepage — aparecem na página do pacote no registry do npm.
  • bin — se o seu pacote oferece uma CLI, é aqui que você mapeia os nomes dos comandos para os arquivos de script. Depois do install, esses comandos ficam executáveis.
  • workspaces — para monorepos; diz ao npm para tratar subpastas como pacotes linkados.

Você não precisa de todos. Precisa dos certos pro que você está fazendo.

Pegadinhas comuns

Algumas coisas que costumam derrubar quem está começando:

  • Commitar node_modules. Não faça isso. Coloque no .gitignore. O package.json junto com o package-lock.json já é suficiente pra qualquer pessoa reconstruir tudo com npm install.
  • Não commitar o package-lock.json. Esse sim, commite. Sem o lockfile, o clássico "na minha máquina funciona" vira uma possibilidade real, porque os ranges de semver podem resolver para versões diferentes com o tempo.
  • Colocar dependências de runtime em devDependencies. Localmente pode funcionar, já que as dev deps estão instaladas, mas em produção quebra, porque lá elas são ignoradas. Se o código que vai pro ar usa, o lugar é em dependencies.
  • Editar versões na mão sem reinstalar. Se você mudar uma versão no package.json, rode npm install em seguida — senão o node_modules e o lockfile ficam dessincronizados.

A seguir: o runtime do Node

O package.json diz ao Node o que é o seu projeto. O runtime do Node decide como ele roda — resolução de módulos, módulos nativos, globais, o event loop rodando por baixo dos panos. É esse o assunto da próxima página.

Perguntas frequentes

Para que serve o package.json?

É o arquivo de manifesto de um projeto Node.js. Ele registra o nome e a versão do projeto, quais pacotes são dependências, quais scripts você pode rodar com npm run e metadados como o entry point e o tipo de módulo. O npm install lê esse arquivo para saber o que precisa baixar.

Qual a diferença entre dependencies e devDependencies?

As dependencies são os pacotes que o seu código precisa para rodar em produção — coisas como express ou react. Já as devDependencies só são usadas durante o desenvolvimento ou build: test runners, bundlers, linters. Quando alguém instala o seu pacote como dependência de outro projeto, o npm ignora as suas devDependencies.

O que significam o ^ e o ~ nas versões do package.json?

São operadores de range do semver. ^1.2.3 aceita qualquer versão 1.x.x a partir de 1.2.3 (mesma major). Já ~1.2.3 é mais restrito — aceita 1.2.x a partir de 1.2.3 (mesma minor). Sem nenhum símbolo, 1.2.3 fixa exatamente essa versão. O package-lock.json guarda as versões exatas resolvidas para que a instalação seja reproduzível.

Como criar um arquivo package.json?

Rode npm init dentro de uma pasta vazia e responda as perguntas, ou use npm init -y para aceitar os padrões e já gerar o arquivo na hora. Também dá para escrever na mão — é só JSON. Os únicos campos realmente obrigatórios são name e version.

Aprenda a programar com o Coddy

COMEÇAR