O npm (Node Package Manager) é o gerenciador de pacotes padrão do ecossistema JavaScript, especialmente para o Node.js. Ele permite instalar, compartilhar e gerenciar bibliotecas e ferramentas de código aberto, facilitando o reuso de código e a colaboração em projetos. Com o npm, você pode declarar as dependências do seu projeto no arquivo package.json, instalar pacotes com um simples comando e executar tarefas automatizadas através de scripts.

Nesta aula, vamos explorar os conceitos fundamentais do npm: o que é o package.json, como instalar dependências, como criar e usar scripts personalizados e como funciona o versionamento semântico (SemVer), que é vital para manter a compatibilidade entre versões de pacotes.

package.json

O package.json é o coração de qualquer projeto Node.js. Ele é um arquivo JSON que contém metadados sobre o projeto, como nome, versão, descrição, ponto de entrada, licença e, crucialmente, as dependências do projeto. Esse arquivo permite que outros desenvolvedores (ou você mesmo em outro ambiente) repliquem o ambiente de desenvolvimento com facilidade, pois todas as dependências estão listadas com suas versões.

O package.json pode ser criado manualmente ou através do comando npm init, que guia o usuário na criação de um arquivo básico. A seguir, um exemplo de um package.json típico:

{
  "name": "meu-projeto",
  "version": "1.0.0",
  "description": "Um projeto de exemplo para aprender npm",
  "main": "index.js",
  "scripts": {
    "start": "node index.js",
    "test": "echo \"Erro: nenhum teste especificado\" && exit 1"
  },
  "keywords": ["npm", "exemplo"],
  "author": "Seu Nome",
  "license": "ISC",
  "dependencies": {
    "express": "^4.18.2"
  },
  "devDependencies": {
    "nodemon": "^3.0.1"
  }
}

Os campos mais importantes são dependencies e devDependencies. O primeiro lista os pacotes necessários para o funcionamento do projeto em produção; o segundo lista pacotes usados apenas durante o desenvolvimento, como ferramentas de teste ou transpiladores. O campo scripts define comandos que podem ser executados com npm run <script>, facilitando tarefas como iniciar o servidor ou rodar testes.

Além disso, o package.json é essencial para o controle de versão: quando você compartilha seu projeto, outras pessoas podem executar npm install para instalar todas as dependências listadas, garantindo que o projeto funcione de forma consistente.

Instalando dependências

Instalar dependências é uma das tarefas mais comuns no npm. O comando npm install <nome-do-pacote> baixa o pacote do registro npm e o adiciona ao package.json automaticamente. Existem variações: npm install --save (padrão) adiciona ao dependencies, enquanto npm install --save-dev adiciona ao devDependencies. Para instalar todas as dependências listadas no package.json (por exemplo, após clonar um repositório), basta executar npm install sem argumentos.

Vamos ver exemplos práticos:

# Instala uma dependência de produção
npm install express

# Instala uma dependência de desenvolvimento
npm install --save-dev nodemon

# Instala uma versão específica
npm install lodash@4.17.21

# Instala um pacote globalmente (para ferramentas de linha de comando)
npm install -g eslint

# Remove um pacote
npm uninstall express

# Lista os pacotes instalados
npm list

Quando você instala um pacote, o npm cria uma pasta node_modules contendo o pacote e suas dependências transitivas. Essa pasta não deve ser versionada; em vez disso, usamos o package-lock.json (gerado automaticamente) para travar as versões exatas instaladas, garantindo reprodutibilidade. O package-lock.json deve ser commitado no controle de versão.

Além disso, o npm oferece o comando npm ci, que instala as dependências exatamente conforme o package-lock.json, sendo mais rápido e determinístico que npm install em ambientes de CI/CD.

Scripts

Os scripts no package.json são atalhos para comandos que você usa com frequência. Eles permitem automatizar tarefas como iniciar o servidor, rodar testes, buildar o projeto, etc. A seção scripts é um objeto onde cada chave é o nome do script e cada valor é o comando a ser executado no shell.

Para executar um script, use npm run <nome-do-script>. Existem alguns scripts especiais que podem ser executados sem o run, como npm start e npm test, pois são convenções populares.

Exemplo de scripts no package.json:

{
  "scripts": {
    "start": "node index.js",
    "dev": "nodemon index.js",
    "build": "webpack --mode production",
    "test": "jest"
  }
}

Para executar esses scripts:

npm start        # executa "node index.js"
npm run dev      # executa "nodemon index.js"
npm run build    # executa "webpack --mode production"
npm test         # executa "jest"

Os scripts também podem ser combinados usando operadores de shell como && (executa o próximo somente se o anterior for bem-sucedido) e ||. Por exemplo, "lint": "eslint . && prettier --check .".

Uma vantagem dos scripts é que eles são portáveis: você não precisa lembrar de comandos complexos, apenas npm run <script>. Além disso, o npm adiciona os binários locais (em node_modules/.bin) ao PATH durante a execução dos scripts, permitindo usar ferramentas como webpack ou jest sem instalá-las globalmente.

Versionamento semântico

O versionamento semântico (SemVer) é um padrão para atribuir versões a pacotes de software, visando comunicar o tipo de mudanças em cada lançamento. O formato é MAJOR.MINOR.PATCH, por exemplo, 1.4.2. Cada parte tem um significado específico:

  • MAJOR: incrementado quando há mudanças incompatíveis com versões anteriores (breaking changes).
  • MINOR: incrementado quando novas funcionalidades são adicionadas de forma compatível com versões anteriores.
  • PATCH: incrementado quando correções de bugs são feitas sem adicionar novas funcionalidades.

Além disso, existem sufixos como -alpha, -beta, -rc para pré-lançamentos, e metadados de build como +build.

No npm, você pode especificar faixas de versão nos dependencies usando operadores. Os mais comuns são:

  • ^1.4.2: aceita versões compatíveis com 1.x.x, ou seja, qualquer versão >= 1.4.2 e < 2.0.0 (mantém o MAJOR).
  • ~1.4.2: aceita versões >= 1.4.2 e < 1.5.0 (mantém o MINOR).
  • 1.4.2: exatamente a versão 1.4.2.
  • >=1.4.2 <2.0.0: faixa explícita.
  • * ou latest: qualquer versão (não recomendado para produção).

Exemplo de utilização:

{
  "dependencies": {
    "express": "^4.18.2",
    "lodash": "~4.17.21",
    "react": "18.2.0"
  }
}

O npm respeita essas faixas ao instalar pacotes, mas o package-lock.json trava as versões exatas que foram instaladas, garantindo que todos os ambientes usem as mesmas versões.

Entender SemVer é crucial para gerenciar dependências com segurança, pois mudanças MAJOR podem quebrar seu código, enquanto MINOR e PATCH geralmente são seguras. Ao publicar seus próprios pacotes, seguir SemVer ajuda a comunidade a confiar nas atualizações.

Boas práticas

Ao trabalhar com npm, algumas boas práticas incluem:

  • Commitar o package-lock.json para garantir instalações reprodutíveis.
  • Usar npm ci em pipelines de CI em vez de npm install.
  • Manter as dependências atualizadas, mas com cautela: use npm update para atualizar dentro das faixas, e npm outdated para ver versões disponíveis.
  • Evitar instalar pacotes globalmente, preferindo usá-los como dependências de desenvolvimento.
  • Documentar os scripts principais no README do projeto.
  • Verificar a integridade dos pacotes com npm audit para vulnerabilidades de segurança.

Referências

Exercícios

  1. Exercício 1: Crie um novo projeto npm chamado meu-app com o comando npm init -y. Depois, adicione a dependência express (versão 4.18.2) e a dependência de desenvolvimento nodemon. Ao final, exiba o conteúdo do package.json.
  2. ✓ Resposta: Execute os comandos no terminal:
    npm init -y
    npm install express@4.18.2
    npm install --save-dev nodemon
    O package.json terá as dependências adicionadas.
  3. Exercício 2: No package.json do projeto, adicione um script chamado dev que execute nodemon index.js. Depois, execute npm run dev e explique o que acontece.
  4. ✓ Resposta: Adicione no package.json:
    "scripts": {
      "dev": "nodemon index.js"
    }
    Ao executar npm run dev, o npm executa o comando nodemon index.js, que inicia o servidor Node e o reinicia automaticamente a cada alteração no código.
  5. Exercício 3: Explique a diferença entre ^4.17.0 e ~4.17.0 em uma faixa de versão do npm.
  6. ✓ Resposta: ^4.17.0 permite qualquer versão >= 4.17.0 e < 5.0.0 (mantém o MAJOR). ~4.17.0 permite >= 4.17.0 e < 4.18.0 (mantém o MINOR). Ou seja, ^ é mais permissivo, aceitando novas funcionalidades, enquanto ~ só aceita correções de bugs.
  7. Exercício 4: O que é o package-lock.json e por que é importante commitá-lo no controle de versão?
  8. ✓ Resposta: O package-lock.json registra as versões exatas de cada pacote instalado, incluindo dependências transitivas. Commitá-lo garante que todos os desenvolvedores e ambientes de CI usem exatamente as mesmas versões, evitando inconsistências e bugs difíceis de reproduzir.
  9. Exercício 5: Suponha que você precise instalar o pacote lodash na versão 4.17.21, mas apenas para desenvolvimento (para usar em testes). Qual comando você usaria?
  10. ✓ Resposta: npm install --save-dev lodash@4.17.21. Isso adiciona ao devDependencies a versão exata.