NPM: gerenciando pacotes e dependências

[110] NPM: gerenciando pacotes e dependências

Todo projeto Node começa com um package.json de quinze linhas e termina com um node_modules de centenas de megabytes. O artigo liga um ao outro: instalação de pacotes, dependencies contra devDependencies, o que os prefixos do SemVer autorizam, scripts com hooks, dotenv e o papel do package-lock.json.
Javascript

21 min de leitura

Uma das maiores vantagens do Node.js não é o que ele faz sozinho — é o que a comunidade construiu em cima dele. O NPM (Node Package Manager) é o maior repositório de código aberto do mundo, com mais de 2 milhões de pacotes disponíveis. Autenticação, envio de email, validação de dados, geração de PDFs, conexão com bancos de dados — existe um pacote para tudo.

Neste artigo você vai aprender a gerenciar dependências com NPM, entender o package.json a fundo, trabalhar com scripts, e adotar boas práticas que fazem diferença em projetos reais.

O que é o NPM

O NPM tem três partes:

1. Registro (registry)
   Site: https://npmjs.com
   Repositório online com todos os pacotes publicados

2. CLI (command line interface)
   Ferramenta de linha de comando instalada com o Node
   Comando: npm

3. package.json
   Arquivo que descreve seu projeto e suas dependências

Iniciando um projeto

# Cria o package.json interativamente
npm init

# Cria com valores padrão (recomendado para começar rápido)
npm init -y

O package.json gerado:

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

Instalando pacotes

# Instala e adiciona em "dependencies" (produção)
npm install express
npm install express mongoose dotenv

# Abreviação
npm i express

# Instala e adiciona em "devDependencies" (apenas desenvolvimento)
npm install --save-dev nodemon jest eslint
npm i -D nodemon jest

# Instala globalmente (disponível em qualquer projeto)
npm install -g nodemon
npm install -g typescript

# Instala versão específica
npm install express@4.18.2

# Instala a mais recente de uma versão maior
npm install express@4

# Instala de um repositório GitHub
npm install github:usuario/repositorio

O package.json em profundidade

{
  "name": "api-clientes",
  "version": "2.1.0",
  "description": "API REST para gerenciamento de clientes",
  "main": "src/index.js",
  "type": "module",

  "scripts": {
    "start": "node src/index.js",
    "dev": "nodemon src/index.js",
    "test": "jest --coverage",
    "test:watch": "jest --watch",
    "lint": "eslint src/",
    "lint:fix": "eslint src/ --fix",
    "build": "tsc",
    "db:migrate": "node src/db/migrate.js",
    "db:seed": "node src/db/seed.js"
  },

  "dependencies": {
    "express": "^4.18.2",
    "mongoose": "^7.6.0",
    "dotenv": "^16.3.1",
    "bcrypt": "^5.1.1",
    "jsonwebtoken": "^9.0.2",
    "zod": "^3.22.4"
  },

  "devDependencies": {
    "nodemon": "^3.0.2",
    "jest": "^29.7.0",
    "eslint": "^8.54.0",
    "@types/express": "^4.17.21"
  },

  "engines": {
    "node": ">=18.0.0",
    "npm": ">=9.0.0"
  },

  "keywords": ["api", "rest", "express", "mongodb"],
  "author": "Ricardo Matos <ricardo@email.com>",
  "license": "MIT",
  "repository": {
    "type": "git",
    "url": "https://github.com/usuario/api-clientes"
  }
}

Versionamento semântico — SemVer

Entender os números de versão é essencial para evitar quebras inesperadas:

MAJOR.MINOR.PATCH
  2  .  1  .  0

MAJOR → quebra compatibilidade com versões anteriores
MINOR → adiciona funcionalidade mantendo compatibilidade
PATCH → correção de bug, sem novas funcionalidades

Os prefixos no package.json:

{
  "dependencies": {
    "express": "4.18.2",    // exatamente esta versão
    "express": "~4.18.2",   // >= 4.18.2, < 4.19.0 (só patches)
    "express": "^4.18.2",   // >= 4.18.2, < 5.0.0  (minor e patches)
    "express": "*",          // qualquer versão (perigoso!)
    "express": ">=4.0.0",   // qualquer versão >= 4
    "express": "4.x"        // qualquer 4.x.x
  }
}

Na prática, ^ (caret) é o padrão — permite updates de minor e patch, mas não de major.

O package-lock.json — reprodutibilidade garantida

# Quando você roda npm install pela primeira vez,
# o NPM cria o package-lock.json

# Este arquivo registra a versão EXATA de cada pacote
# e de cada dependência das dependências (árvore completa)
// package-lock.json (trecho)
{
  "name": "meu-projeto",
  "version": "1.0.0",
  "lockfileVersion": 3,
  "packages": {
    "node_modules/express": {
      "version": "4.18.2",
      "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
      "integrity": "sha512-...",
      "dependencies": {
        "accepts": "~1.3.8",
        "body-parser": "1.20.1"
      }
    }
  }
}
# Instala as versões EXATAS do package-lock.json
# Use em produção e CI/CD para garantir reprodutibilidade
npm ci

# vs npm install → pode atualizar dentro das regras do ^

Regra de ouro: sempre commite o package-lock.json no Git. Nunca commite node_modules.

O node_modules e o .gitignore

# O node_modules pode ter centenas de MB
# NUNCA commite no Git

# .gitignore
node_modules/
.env
.env.local
dist/
build/
*.log
# Para restaurar os node_modules em outro computador:
git clone https://github.com/usuario/projeto
cd projeto
npm install  # lê o package.json e restaura tudo

Scripts NPM — automatizando tarefas

Os scripts são um dos recursos mais poderosos do NPM:

{
  "scripts": {
    "start": "node src/index.js",
    "dev": "nodemon src/index.js --watch src",
    "test": "jest",
    "test:coverage": "jest --coverage",
    "lint": "eslint .",
    "format": "prettier --write .",
    "clean": "rm -rf dist node_modules",
    "build": "npm run lint && npm run test && node build.js",
    "prepare": "npm run build",
    "postinstall": "node scripts/setup.js"
  }
}
# Rodar scripts
npm start           # convenção para produção
npm test            # convenção para testes
npm run dev         # scripts customizados precisam de "run"
npm run lint
npm run build

# Hooks automáticos — executam antes/depois
# pre[script] → executa ANTES do script
# post[script] → executa DEPOIS do script
# npm run build → executa: prebuild → build → postbuild

Pacotes essenciais — os que você vai usar sempre

Desenvolvimento:

# Reinicia o servidor automaticamente ao salvar arquivos
npm i -D nodemon

# Linter — encontra problemas no código
npm i -D eslint

# Formatador — padroniza estilo de código
npm i -D prettier

# Framework de testes
npm i -D jest

Produção:

# Framework web
npm i express

# Variáveis de ambiente
npm i dotenv

# Validação de dados
npm i zod

# Hash de senhas
npm i bcrypt

# JWT (autenticação)
npm i jsonwebtoken

# ORM para MongoDB
npm i mongoose

# ORM para SQL
npm i prisma

# Requisições HTTP (alternativa ao fetch nativo)
npm i axios

# Datas e horas
npm i date-fns

dotenv — variáveis de ambiente

Nunca coloque senhas, tokens ou URLs de banco de dados diretamente no código:

# Instalar
npm i dotenv
# .env (nunca commite este arquivo!)
PORT=3000
NODE_ENV=development
DATABASE_URL=mongodb://localhost:27017/meuapp
JWT_SECRET=uma-string-secreta-muito-longa-e-aleatoria
EMAIL_HOST=smtp.gmail.com
EMAIL_USER=meu@email.com
EMAIL_PASS=senha-do-email
// No início da aplicação — antes de tudo
require("dotenv").config();
// ou com ES Modules:
import "dotenv/config";

// Agora process.env tem as variáveis do .env
const porta = process.env.PORT || 3000;
const dbUrl = process.env.DATABASE_URL;
const secret = process.env.JWT_SECRET;

if (!dbUrl) {
  console.error("❌ DATABASE_URL não definida!");
  process.exit(1);
}

console.log(`Rodando em modo: ${process.env.NODE_ENV}`);
# .env.example — versão sem valores reais, ESTE sim vai no Git
PORT=3000
NODE_ENV=development
DATABASE_URL=
JWT_SECRET=
EMAIL_HOST=
EMAIL_USER=
EMAIL_PASS=

nodemon — desenvolvimento ágil

Sem nodemon, você precisaria parar e reiniciar o servidor manualmente a cada mudança:

Antes de instalar, saiba que o Node já faz isso sozinho. Desde a versão 18.11 existe a flag --watch, estável a partir do Node 20, e para o caso comum ela dispensa a dependência por completo:

// package.json — sem instalar nada
{
  "scripts": {
    "dev": "node --watch src/index.js",
    // --watch-path limita o que é observado; sem ele, o Node segue os
    // arquivos que o seu código realmente importa, o que já costuma bastar
    "dev:src": "node --watch-path=./src src/index.js",
    // e este recarrega também o .env a cada mudança (Node 20+)
    "dev:env": "node --watch --env-file=.env src/index.js"
  }
}

O nodemon continua útil quando você precisa de mais controle — observar extensões específicas, ignorar pastas, aplicar um atraso antes de reiniciar, executar um comando que não é node. Para node src/index.js e nada mais, o nativo basta.

⚠️ Windows com WSL, e Docker com volume montado: se o projeto está numa pasta do Windows acessada pelo WSL — qualquer caminho sob /mnt/c, /mnt/do watch simplesmente não dispara. O motivo é que a notificação de mudança de arquivo no Linux usa o inotify, e ele não atravessa a fronteira entre o sistema de arquivos do Windows e o do Linux: você salva, nada acontece, e a impressão é de que a ferramenta está quebrada. A saída é trocar a detecção por evento pela verificação periódica, o chamado polling:

# -L é o atalho de --legacy-watch: o nodemon passa a CONSULTAR os arquivos
# em intervalos, em vez de esperar um evento que nunca chega.
nodemon -L src/index.js

# no package.json, para o time inteiro:
#   "dev": "nodemon -L src/index.js"

# ou pelo nodemon.json, que é mais limpo:
#   { "legacyWatch": true, "pollingInterval": 1000 }

# O mesmo vale em Docker quando o código vem de um volume do host:
#   CHOKIDAR_USEPOLLING=true npm run dev

Duas ressalvas que valem o parágrafo. A primeira é que o node --watch sofre do mesmo problema e não tem equivalente ao -L — nesse ambiente específico, o nodemon ainda é a resposta. A segunda é que o polling custa CPU, porque varre os arquivos de tempo em tempo; num projeto grande isso aparece no ventilador. A solução de fundo, quando possível, é manter o projeto dentro do sistema de arquivos do Linux (algo como ~/projetos, e não /mnt/d/projetos) — além de o watch voltar a funcionar por evento, a leitura de arquivo fica várias vezes mais rápida, o que o npm install agradece.

npm i -D nodemon
// package.json
{
  "scripts": {
    "dev": "nodemon src/index.js"
  }
}
// nodemon.json (configuração opcional)
{
  "watch": ["src"],
  "ext": "js,json",
  "ignore": ["src/tests/**"],
  "delay": 500,
  "env": {
    "NODE_ENV": "development"
  }
}
npm run dev
# [nodemon] watching path(s): src/**/*
# [nodemon] starting `node src/index.js`
# Servidor rodando na porta 3000
# (você salva um arquivo...)
# [nodemon] restarting due to changes...
# [nodemon] starting `node src/index.js`
# Servidor rodando na porta 3000

Comandos NPM essenciais

# ── Instalação ───────────────────────────────────
npm install                    # instala tudo do package.json
npm ci                         # instala versões exatas do lock
npm i pacote                   # instala e salva em dependencies
npm i -D pacote                # instala e salva em devDependencies
npm i -g pacote                # instala globalmente

# ── Remoção ──────────────────────────────────────
npm uninstall pacote           # remove e atualiza package.json
npm uninstall -g pacote        # remove global

# ── Atualização ──────────────────────────────────
npm update                     # atualiza tudo (dentro do semver)
npm update pacote              # atualiza pacote específico
npm outdated                   # lista pacotes desatualizados

# ── Informações ──────────────────────────────────
npm list                       # lista dependências instaladas
npm list --depth=0             # apenas nível 1 (sem transitivas)
npm info pacote                # informações do pacote no registry
npm search termo               # busca no registry

# ── Auditoria de segurança ────────────────────────
npm audit                      # verifica vulnerabilidades
npm audit fix                  # tenta corrigir automaticamente

# ── Cache ─────────────────────────────────────────
npm cache clean --force        # limpa o cache

# ── Publicação ───────────────────────────────────
npm login                      # autentica no registry
npm publish                    # publica o pacote
npm version patch              # incrementa patch (1.0.0 → 1.0.1)
npm version minor              # incrementa minor (1.0.0 → 1.1.0)
npm version major              # incrementa major (1.0.0 → 2.0.0)

Alternativas ao NPM

# Yarn — criado pelo Facebook, mais rápido no passado
npm i -g yarn
yarn add express
yarn add -D nodemon
yarn install

# pnpm — mais eficiente em espaço em disco
# usa links simbólicos para compartilhar pacotes entre projetos
npm i -g pnpm
pnpm add express
pnpm add -D nodemon
pnpm install

# bun — runtime e gerenciador de pacotes ultra-rápido (2023)
# compatível com NPM
bun add express
bun install

Para projetos novos em 2025, pnpm é uma excelente escolha pela eficiência. NPM ainda é o padrão da indústria e o mais universal.

Exemplo completo — estrutura de projeto profissional

meu-projeto/
├── src/
│   ├── index.js          # ponto de entrada
│   ├── config/
│   │   └── index.js      # configurações centralizadas
│   ├── routes/           # rotas da API
│   ├── controllers/      # lógica das rotas
│   ├── services/         # regras de negócio
│   ├── models/           # modelos de dados
│   └── utils/            # utilitários
├── tests/                # testes
├── .env                  # variáveis (não vai no Git)
├── .env.example          # modelo (vai no Git)
├── .gitignore
├── .eslintrc.json
├── nodemon.json
├── package.json
└── package-lock.json
// src/config/index.js — configuração centralizada
require("dotenv").config();

const config = {
  porta: Number(process.env.PORT) || 3000,
  nodeEnv: process.env.NODE_ENV || "development",
  dbUrl: process.env.DATABASE_URL,
  jwtSecret: process.env.JWT_SECRET,

  isDev: process.env.NODE_ENV === "development",
  isProd: process.env.NODE_ENV === "production",
  isTest: process.env.NODE_ENV === "test",
};

// Valida variáveis obrigatórias
const obrigatorias = ["DATABASE_URL", "JWT_SECRET"];
const faltando = obrigatorias.filter(v => !process.env[v]);

if (faltando.length > 0) {
  console.error(`❌ Variáveis de ambiente faltando: ${faltando.join(", ")}`);
  process.exit(1);
}

module.exports = config;

Tarefa para você

Crie um projeto Node.js do zero com a seguinte estrutura:

# 1. Inicie o projeto com npm init -y
# 2. Instale as dependências:
#    - dotenv (produção)
#    - nodemon (dev)
#    - date-fns (produção)

# 3. Configure os scripts:
#    - start: node src/index.js
#    - dev: nodemon src/index.js
#    - info: node src/info.js

# 4. Crie src/info.js que imprime:
#    - Nome e versão do projeto (lidos do package.json)
#    - Data e hora atual formatada com date-fns
#    - Variáveis de ambiente: PORT e NODE_ENV
#    - Informações do sistema: OS, Node version, memória

# 5. Crie um .env com PORT=3000 e NODE_ENV=development
# 6. Crie um .gitignore adequado
# 7. Crie um .env.example documentado

# Bônus: use o package.json como fonte de verdade
// src/info.js
const pkg = require("../package.json");
console.log(`${pkg.name} v${pkg.version}`);
Ver solução — o projeto inteiro: package.json, .env, .gitignore e o info.js
// Os comandos, na ordem:
//
//   npm init -y
//   npm install dotenv date-fns
//   npm install --save-dev nodemon
//
// `--save-dev` não é detalhe: nodemon em `dependencies` vai junto para
// produção, incha a imagem e instala um observador de arquivos num
// servidor que não precisa dele.

// package.json
// {
//   "name": "projeto-npm",
//   "version": "1.0.0",
//   "description": "Exercício de NPM: scripts, dependências e ambiente",
//   "main": "src/index.js",
//   "type": "commonjs",
//   "scripts": {
//     "start": "node src/index.js",
//     "dev": "nodemon src/index.js",
//     "info": "node src/info.js"
//   },
//   "engines": { "node": ">=18" },
//   "dependencies": {
//     "date-fns": "^3.6.0",
//     "dotenv": "^16.4.5"
//   },
//   "devDependencies": {
//     "nodemon": "^3.1.0"
//   }
// }
//
// O `engines` documenta o mínimo — e evita o chamado de suporte de
// quem rodou no Node 14 e recebeu erro de sintaxe.

// .env  (NUNCA versionado)
//   PORT=3000
//   NODE_ENV=development

// .env.example  (versionado, sem valor real — é a documentação)
//   # Porta do servidor HTTP
//   PORT=3000
//   # development | production | test
//   NODE_ENV=development
//   # Chave da API de pagamentos — peça ao time, não invente
//   API_KEY=

// .gitignore
//   node_modules/
//   .env
//   .env.local
//   *.log
//   npm-debug.log*
//   coverage/
//   dist/
//   .DS_Store
//
// `node_modules/` fica de fora porque o package-lock.json já descreve
// exatamente o que instalar. Versionar a pasta é subir dezenas de
// milhares de arquivos que o `npm ci` reconstrói em segundos.
//
// `.env` fica de fora porque contém segredo. Se ele já foi commitado
// alguma vez, tirar do repositório NÃO basta: continua no histórico.
// A credencial tem de ser trocada.

// ---- src/info.js
require("dotenv").config();

const os = require("node:os");
const { format } = require("date-fns");
const { ptBR } = require("date-fns/locale");

// O package.json como fonte de verdade: nome e versão não são
// repetidos no código, então não há como divergirem.
const pkg = require("../package.json");

const emMB = (bytes) => `${(bytes / 1024 / 1024).toFixed(0)} MB`;

// process.env é SEMPRE string — inclusive "3000" e "false". Sem
// conversão, `PORT + 1` daria "30001", e `Boolean("false")` é true.
const porta = Number(process.env.PORT ?? 3000);
const ambiente = process.env.NODE_ENV ?? "development";

console.log(`${pkg.name} v${pkg.version}`);
console.log(pkg.description);
console.log("─".repeat(48));

console.log(`Data      : ${format(new Date(), "dd/MM/yyyy 'às' HH:mm:ss", { locale: ptBR })}`);
console.log(`Dia       : ${format(new Date(), "EEEE", { locale: ptBR })}`);
console.log("─".repeat(48));

console.log(`PORT      : ${porta}`);
console.log(`NODE_ENV  : ${ambiente}`);

// Aviso útil: variável ausente costuma aparecer como bug esquisito
// três camadas adiante, não como erro no boot.
for (const chave of ["PORT", "NODE_ENV"]) {
  if (process.env[chave] === undefined) {
    console.warn(`⚠️  ${chave} não definida — usando o padrão. Copie o .env.example.`);
  }
}

console.log("─".repeat(48));
console.log(`SO        : ${os.type()} ${os.release()} (${os.arch()})`);
console.log(`Node      : ${process.version}`);
console.log(`CPUs      : ${os.cpus().length}`);
console.log(`Memória   : ${emMB(os.totalmem() - os.freemem())} em uso de ${emMB(os.totalmem())}`);
console.log(`Uptime    : ${(os.uptime() / 3600).toFixed(1)} h`);

// ---------------------------------------------------------------
// O detalhe que morde: ^ no package.json e o package-lock
// ---------------------------------------------------------------
// "date-fns": "^3.6.0" aceita qualquer 3.x — 3.7, 3.9, 3.20. Quem
// instalar amanhã pode receber código diferente do seu, e o bug que
// só acontece na máquina de um colega nasce daí.
//
// Quem congela isso é o package-lock.json, que registra a versão
// exata de cada pacote e de cada dependência das dependências. Por
// isso ele É versionado, e por isso o CI usa:
//
//   npm ci     ← instala exatamente o lock, apaga node_modules antes
//   npm install ← pode ATUALIZAR o lock, é para desenvolvimento
//
// Usar `npm install` no CI é abrir mão da reprodutibilidade
// silenciosamente.

process.env devolve string sempre: PORT + 1 vira "30001" e Boolean("false") é true. Converta na entrada. E no CI use npm ci, não npm install — só o primeiro respeita o package-lock.json à risca.

Dois arquivos governam um projeto Node: o package.json, que declara o que você aceita, e o package-lock.json, que registra o que foi de fato instalado. Confundir o papel dos dois é a origem do "na minha máquina funciona" — daí o lock ir versionado e o CI usar npm ci, nunca npm install. O resto é vocabulário: -D para o que só serve em desenvolvimento, scripts para não decorar comando, e o .env fora do Git, sempre.

Fontes e Referências

Exercícios

Exercício 1

Estas quatro linhas estão no package.json. Que versões cada uma autoriza o NPM a instalar?

{
  "dependencies": {
    "express": "^4.18.2",
    "express-rate-limit": "^0.5.1",
    "mongoose": "~7.6.0",
    "zod": "*"
  }
}
Ver resposta

✓ Resposta: ^4.18.2 aceita de 4.18.2 até abaixo de 5.0.0; ~7.6.0 aceita de 7.6.0 até abaixo de 7.7.0, só patches; * aceita qualquer coisa, inclusive a próxima major que quebra tudo. A segunda linha é a pegadinha: ^0.5.1 não permite até a 1.0.0. Abaixo da versão 1, o caret se comporta como o til e trava no minor — de 0.5.1 até abaixo de 0.6.0. A regra existe porque o SemVer considera que a fase 0.x é instável por definição, e ali a própria especificação libera o autor a quebrar compatibilidade num minor. Consequência prática: um pacote em 0.x não recebe correção nenhuma pelo npm update quando o autor publica a 0.6, e isso passa despercebido justamente em bibliotecas pequenas, que são as que mais vivem em 0.x. Vale saber ainda que ^ e ~ descrevem o que é permitido, não o que está instalado — quem responde isso é o package-lock.json.

Exercício 2

O código funciona na sua máquina e quebra no servidor de integração, sem que ninguém tenha mudado uma linha. O package-lock.json está commitado. O que explica a diferença?

# na sua máquina, três meses atrás:
npm install

# no pipeline, hoje:
npm install
Ver resposta

✓ Resposta: Porque npm install tem licença para atualizar o lock. Ele lê o package.json, e sempre que uma versão mais nova ainda satisfaz o intervalo do ^, ele a instala e reescreve o package-lock.json de acordo. Três meses depois, ^4.18.2 pode significar 4.19.7 — outro código, com outro bug. O comando para ambiente automatizado é npm ci: ele exige o lock, apaga o node_modules antes, instala exatamente as versões registradas e falha se o lock estiver dessincronizado do package.json — que é justamente o aviso que você quer receber. Três consequências que vêm junto: o npm ci nunca altera nenhum dos dois arquivos, então um pipeline não pode "consertar" o lock por acidente; ele é bem mais rápido, porque não precisa resolver a árvore de dependências; e, como falha em vez de improvisar, um lock esquecido vira erro de build em vez de um deploy silenciosamente diferente. A regra curta: npm install é para quando você quer mudar as dependências, npm ci para todo o resto.

Exercício 3

O deploy usa npm ci --omit=dev para economizar espaço na imagem. O servidor sobe e morre na hora. Onde está o erro?

{
  "scripts": {
    "start": "nodemon src/index.js"
  },
  "dependencies": {
    "express": "^4.18.2",
    "dotenv": "^16.4.5"
  },
  "devDependencies": {
    "nodemon": "^3.1.0",
    "jest": "^29.7.0"
  }
}
Ver resposta

✓ Resposta: O start chama o nodemon, que é uma devDependency — e o --omit=dev não o instalou. O processo morre com sh: nodemon: not found, uma mensagem que aponta para o ambiente quando o erro está na configuração. O start é a porta de entrada de produção e só pode usar o que existe em produção: "start": "node src/index.js", deixando o nodemon no dev, que é a convenção que o artigo usa. Repare que a separação entre os dois grupos não é tecnicamente imposta: em desenvolvimento, um npm install normal instala os dois e tudo funciona igual, e é por isso que o engano só aparece no deploy. Vale conhecer o erro simétrico, mais comum e mais caro: colocar em devDependencies algo de que o runtime precisa — um driver de banco, um validador — porque só apareceu na fase de testes. O sintoma é o mesmo, Cannot find module, em produção. O teste mental é direto: se este pacote sumisse do servidor, a aplicação ainda atende uma requisição?

Exercício 4

O .env foi commitado por engano há dois dias, com a senha do banco e a chave da API de pagamentos. Alguém abre este PR. Ele resolve?

echo ".env" >> .gitignore
git add .gitignore
git commit -m "chore: ignora o .env"
git push
Ver resposta

✓ Resposta: Não resolve nada, e por dois motivos independentes. O primeiro é mecânico: o .gitignore só vale para arquivos não rastreados. O .env já está no índice, então o Git continua versionando cada alteração dele como se o .gitignore não existisse — para pará-lo é preciso git rm --cached .env, que o remove do rastreamento sem apagar o arquivo do disco. O segundo é o que realmente importa: mesmo depois de removido, o segredo continua no histórico, em cada commit dos últimos dois dias, e qualquer pessoa com acesso ao repositório — ou com um clone antigo — o recupera com um comando. Para um repositório público, assuma que já foi lido: existem robôs varrendo commits novos em busca exatamente disso, e o intervalo entre o push e a primeira tentativa de uso costuma ser de minutos. Por isso a primeira providência não é mexer no Git, é rotacionar as credenciais: trocar a senha do banco e revogar a chave de pagamentos. Limpar o histórico vem depois, é operação destrutiva, reescreve os hashes e obriga todo mundo a refazer o clone — e não devolve a senha que já vazou.

Exercício 5

Dois desenvolvedores rodam o mesmo comando no mesmo projeto e recebem resultados diferentes. O CI, um terceiro. Por quê — e o que a segunda forma muda?

# como cada um instalou, uma vez, há muito tempo:
npm install -g eslint

# e como rodam:
eslint src/

# a alternativa:
npx eslint src/
Ver resposta

✓ Resposta: Porque o -g instala fora do projeto: cada máquina tem a versão que instalou no dia em que instalou, e nada no repositório diz qual deveria ser. Um roda a 8, outro a 9 — que mudou o formato do arquivo de configuração —, o CI roda a que estiver na imagem, e o resultado é a discussão sobre "na minha máquina passa" aplicada ao próprio linter. O npx muda a origem da ferramenta: ele procura primeiro em node_modules/.bin do projeto, ou seja, usa a versão que está no package.json e travada no package-lock.json, igual para todo mundo. Por isso a forma correta é npm i -D eslint mais um script — "lint": "eslint src/" —, já que os scripts do NPM também enxergam o node_modules/.bin e dispensam o npx na linha de comando. Duas ressalvas úteis: quando o pacote não está instalado, o npx baixa uma cópia temporária e executa, o que é ótimo para rodar um gerador uma vez só (npx create-vite) e péssimo como dependência diária, porque volta a depender do que houver no registro naquele instante; e como ele executa código baixado na hora, um nome digitado errado é um vetor de ataque conhecido — conferir o nome do pacote antes de dar Enter não é paranoia.

Comentários

Mais em Javascript

Funções: declaração, expressão e arrow functions
Funções: declaração, expressão e arrow functions

Uma função sem return devolve undefined — e uma arrow com chaves, mas sem…

APIs públicas: exemplos práticos
APIs públicas: exemplos práticos

Seis APIs públicas, nenhuma exigindo cadastro ou chave: ViaCEP para endereço…

Performance em aplicações web
Performance em aplicações web

É fácil otimizar a coisa errada, e por isso a ordem importa: medir antes. O…