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.
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>:
Algumas coisas importantes sobre scripts:
npm start,npm teste alguns outros nomes específicos funcionam sem a palavrarun. Todo o resto precisa denpm run <nome>.- Os scripts rodam em um shell com
node_modules/.binnoPATH, então dá para chamar binários dos pacotes instalados direto."test": "vitest"funciona mesmo sem ovitestestar 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>epost<nome>rodam automaticamente. Se você tiver umprebuild, ele roda antes dobuildsem 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.
typedefine como os arquivos.jssã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 querequire("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:
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. Opackage.jsonjunto com opackage-lock.jsonjá é suficiente pra qualquer pessoa reconstruir tudo comnpm 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 é emdependencies. - Editar versões na mão sem reinstalar. Se você mudar uma versão no
package.json, rodenpm installem seguida — senão onode_modulese 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.