Dia 2 — npm e o gerenciador de pacotes

Informatica · Conteudo · publicado em 30/09/2026
Dia 2 de 16

npm e o gerenciador de pacotes

Aula 1

npm: instalar pacotes

npm: instalar pacotes

npm é o gerenciador de pacotes do Node: instala dependências, grava o que

instalou e trava o resultado exato para a próxima máquina. Ele já vem com o

Node — não há nada a instalar à parte.

O comando completo é npm install, abreviado para npm i. Instalar é o

gesto mais repetido do projeto inteiro e não é o mesmo que declarar: o `npm

install copia os arquivos para node_modules` e grava o pacote no

package.json ao mesmo tempo, e é essa simultaneidade que faz funcionar.

npm init -y            # cria o package.json do projeto
npm i mysql2           # instala e grava em "dependencies"
npm i -D nodemon       # instala e grava em "devDependencies"
npm uninstall mysql2   # remove do manifesto e da pasta
npm ci                 # instala exatamente o que o lockfile manda

Os três arquivos, e o que cada um é

ArquivoGuardaVai para o git
package.jsono que o projeto declara precisarsim
package-lock.jsona árvore exata que foi instaladasim
node_modules/os arquivos instaladosnão

node_modules é reconstruível: npm ci recria a pasta idêntica a partir dos

outros dois. É por isso que ele nunca é versionado, e um .gitignore com

node_modules/ e .env resolve os dois problemas de uma vez.

dependencies e devDependencies

dependencies é o que o programa precisa para funcionar: mysql2, dotenv. É o

que o termo dependência de produção quer dizer — o pacote faz parte do produto

entregue, e sem ele o programa nem sobe. devDependencies é o que só serve

para desenvolver: formatador, servidor de recarga, ferramenta de teste. Em

produção se instala só o primeiro grupo, e o programa continua inteiro. O

package.json deste material tem só dependencies, porque mysql2 é a única

coisa de que os exemplos dependem.

A distinção não é teórica: é o que decide se uma imagem de produção carrega o

servidor de recarga por cima do código que vai atender requisição.

O careto e o lockfile

Em dependencies, a regra se chama semver, e o ^1.0.0 é a forma mais comum

dela: o 1.0.0 é a versão mínima e o 1.0.0 do ^ significa "a partir

dela e antes da 2.0.0". Aplicado a 3.24.5, ^3.24.5 significa "a partir de

3.24.5 e antes da 4.0.0": qualquer 3.x mais novo entra. ~3.24.5 aperta

para <3.25.0. Sem versão, o npm assume ^.

FormaAceita
^3.24.5>=3.24.5 <4.0.0
~3.24.5>=3.24.5 <3.25.0
3.24.5exatamente 3.24.5

O ^ sozinho explica "funciona na minha máquina e não na sua": duas pessoas com

o mesmo package.json podem instalar 3.24.5 e 3.30.1. Quem impede isso é o

package-lock.json, que grava a versão exata de cada pacote da árvore,

inclusive das dependências indiretas — as que vieram junto sem ninguém pedir. Em

projeto de time é npm ci, que instala a partir do lockfile e falha se ele não

bater com o manifesto.

createRequire e require.resolve

createRequire(caminho) monta um require ancorado num arquivo, e

require.resolve('pacote') diz o caminho que o require carregaria, sem

carregar. É assim que se descobre uma segunda cópia da mesma dependência na

árvore, que é a causa clássica de "a versão é a errada mesmo depois do install".

Exemplo

'use strict';

// Exemplo da aula 1 do dia 2: como o npm guarda o que esta instalado.
//
// O `package.json` DECLARA o que o projeto precisa; o `node_modules` GUARDA o que
// foi instalado; o `package-lock.json` TRAVA a exata arvore de dependencias.
// Sao tres coisas diferentes, e misturar as tres e a origem de "funciona na minha
// maquina e nao na sua".

const fs = require('node:fs');
const path = require('node:path');
const { createRequire } = require('node:module');

// O exemplo mora em `codigo/t2/dia02/`, e o projeto mora tres niveis acima.
// `__dirname` e o diretorio DESTE arquivo, que nao muda com o `cd`: e por isso
// que ele, e nao `process.cwd()`, que se usa para chegar na raiz do projeto.
const RAIZ = path.join(__dirname, '..', '..', '..');

// `createRequire` monta um `require` ancorado num arquivo — aqui, o manifesto do
// projeto. E o mesmo mecanismo que o `require` usa por baixo, e e ele que diz de
// onde um pacote foi carregado.
const exigir = createRequire(path.join(RAIZ, 'package.json'));

const manifesto = JSON.parse(fs.readFileSync(path.join(RAIZ, 'package.json'), 'utf8'));
console.log('nome do projeto:', manifesto.name);
console.log('tipo de modulo:', manifesto.type || 'commonjs (padrao quando a chave falta)');
console.log('dependencias declaradas:', Object.keys(manifesto.dependencies || {}).join(', ') || '(nenhuma)');
console.log('dependencias de desenvolvimento:', Object.keys(manifesto.devDependencies || {}).join(', ') || '(nenhuma)');

// A regra do `^`: `^3.24.5` aceita qualquer 3.x a partir de 3.24.5, e nao a 4.0.0.
// E por isso que o lockfile importa — sem ele, duas maquinas instalam versoes
// diferentes a partir do mesmo manifesto.
console.log('');
console.log('^3.24.5 permite: >=3.24.5 <4.0.0  (o 3.30.1 entra, o 4.0.0 nao)');
console.log('~3.24.5 permite: >=3.24.5 <3.25.0  (so patch e minor)');
console.log('3.24.5  permite: exatamente 3.24.5');

// O pacote instalado e o que o Node vai carregar. O careto esta entre o que o
// manifesto pede e o que chegou na pasta.
console.log('');
console.log('manifesto pede:', manifesto.dependencies.mysql2);
const instalado = JSON.parse(
  fs.readFileSync(exigir.resolve('mysql2/package.json'), 'utf8')
).version;
console.log('instalado de verdade:', instalado);
console.log('carregado de:', path.relative(RAIZ, exigir.resolve('mysql2')));

// O sub-caminho `/promise` e o mesmo pacote, outra porta de entrada.
console.log('entrada /promise:', path.relative(RAIZ, exigir.resolve('mysql2/promise')));

// --- `dependencies` declarado x `dependencies` instalado ---
//
// O manifesto pede UM pacote. A pasta tem onze: os outros dez veio junto como
// dependencia indireta, e e por isso que `npm uninstall` nunca apaga a pasta
// inteira — o que ele apaga e o que ninguem mais pede.
const pedidos = Object.keys(manifesto.dependencies || {});
const instalados = fs.readdirSync(path.join(RAIZ, 'node_modules'))
  .filter((nome) => !nome.startsWith('.'))
  .flatMap((nome) => (nome.startsWith('@')
    ? fs.readdirSync(path.join(RAIZ, 'node_modules', nome)).map((sub) => nome + '/' + sub)
    : [nome]));

console.log('');
console.log('declarado em dependencies:', pedidos.length, '->', pedidos.join(', '));
console.log('instalado em node_modules:', instalados.length);
console.log('veio junto, sem ninguem pedir:');
for (const nome of instalados.filter((n) => !pedidos.includes(n))) {
  const v = JSON.parse(
    fs.readFileSync(path.join(RAIZ, 'node_modules', nome, 'package.json'), 'utf8')
  ).version;
  console.log('  ' + nome + '@' + v);
}

// O lockfile e o arquivo que trava essa arvore inteira. Sem ele, o manifesto
// acima nao impede que a proxima instalacao pegue outras versoes.
console.log('');
if (fs.existsSync(path.join(RAIZ, 'package-lock.json'))) {
  const lock = JSON.parse(fs.readFileSync(path.join(RAIZ, 'package-lock.json'), 'utf8'));
  console.log('package-lock.json trava', Object.keys(lock.packages || {}).length, 'caminhos de pacote');
} else {
  console.log('package-lock.json ausente: cada instalacao pode resolver versoes diferentes');
}

console.log('');
console.log('para o git:  package.json + package-lock.json');
console.log('fora do git: node_modules/  (reconstruivel com npm ci)');
console.log('fora do git: .env            (o segredo, e o assunto da aula 2 de hoje)');
console.log('');
console.log('comandos: npm i <pacote> | npm i -D <pacote> | npm uninstall <pacote> | npm ci | npm outdated');

Saída real

nome do projeto: materiais-node-mysql
tipo de modulo: commonjs
dependencias declaradas: mysql2
dependencias de desenvolvimento: (nenhuma)

^3.24.5 permite: >=3.24.5 <4.0.0  (o 3.30.1 entra, o 4.0.0 nao)
~3.24.5 permite: >=3.24.5 <3.25.0  (so patch e minor)
3.24.5  permite: exatamente 3.24.5

manifesto pede: ^3.24.5
instalado de verdade: 3.24.5
carregado de: node_modules/mysql2/index.js
entrada /promise: node_modules/mysql2/promise.js

declarado em dependencies: 1 -> mysql2
instalado em node_modules: 12
veio junto, sem ninguem pedir:
  @types/[email protected]
  [email protected]
  [email protected]
  [email protected]
  [email protected]
  [email protected]
  [email protected]
  [email protected]
  [email protected]
  [email protected]
  [email protected]

package-lock.json ausente: cada instalacao pode resolver versoes diferentes

para o git:  package.json + package-lock.json
fora do git: node_modules/  (reconstruivel com npm ci)
fora do git: .env            (o segredo, e o assunto da aula 2 de hoje)

comandos: npm i <pacote> | npm i -D <pacote> | npm uninstall <pacote> | npm ci | npm outdated
Aula 2

scripts e organização do projeto

scripts e organização do projeto

O bloco scripts do package.json transforma um comando longo em um nome. O

campo type decide como o Node lê os arquivos .js do projeto — e essa escolha

é do arquivo, não existe um interruptor global.

{
  "type": "commonjs",
  "scripts": {
    "start": "node src/server.js",
    "dev": "node --watch src/server.js"
  }
}
ComandoO que faz
npm startroda o script chamado start
npm run devroda o script chamado dev
npm runlista os scripts do projeto
npm testatalho para o script test

type aceita commonjs (padrão) e module. Com module, .js passa a ser

ESM e o require some: é import. Com commonjs, import num .js não

funciona. A decisão é por projeto, e o package.json é quem a guarda — os dois

modos estão na aula de ESM do dia 3.

A estrutura de projeto

Organizar arquivos é a decisão que mais economiza dor depois, e a convenção do

ecossistema cabe em uma frase: o código fica numa pasta src, e nada além dela

entra no git.

CaminhoO que é
src/server.jso ponto de entrada, quem sobe o processo
src/db.jsa conexão com o banco
src/rotas/um arquivo por arquivo de rota
package.jsonmanifesto e scripts
node_modules/dependências instaladas, fora do git

Duas regras resolvem quase tudo. A primeira é a pasta src: sair dela para a

raiz transforma cada arquivo novo em conflito de merge, porque package.json

e .gitignore são um só e todo mundo mexe. A segunda é a entrada única — um

arquivo que sobe o servidor, e todo o resto é função que esse arquivo chama.

O caminho relativo entre eles é o ./ do require e do import: de dentro de

src/rotas/alunos.js, o banco está em ../db.js e o servidor em

../server.js. Esse ../ é o que a estrutura de projeto torna possível — sem

pasta, o caminho relativo não existe.

.env e variável de ambiente

Variável de ambiente é chave e valor que o sistema entrega ao processo na

partida, sem estar no código. process.env é o objeto que as guarda. É o

mecanismo padrão de configuração no Node, e o motivo é concreto: senha em

variável de ambiente não aparece na listagem de processos de quem olha de fora,

enquanto senha em arquivo versionado aparece para todo mundo que leu o git log.

OndeVai para o gitTem valor real
env.exemplosimnão, é modelo
.envnão (.gitignore)sim
process.envnão existe em discovive só no processo

O .env é um texto com uma variável por linha, no formato CHAVE=valor. Quem o

lê é o pacote dotenv, com uma chamada só, no começo do programa, antes de

qualquer outra linha de código:

require('dotenv').config();

Depois disso process.env.DB_PASS existe no processo. O nome da variável é o

que vai para o material e para o git; o valor não vai para lugar nenhum — e

é por isso que a saída de um exemplo de .env diz apenas se a senha chegou, e

nunca qual é ela.

NODE_ENV e a configuração por ambiente

NODE_ENV é a convenção do ecossistema Node: production em deploy,

development (ou ausente) no resto. O mesmo código muda de comportamento lendo

essa variável — é assim que um programa desliga log detalhado, troca mensagem de

erro técnica por mensagem genérica e liga o modo restrito, sem recompilar nada.

NODE_ENV=test é o que o npm test define por baixo, e é a razão de um

process.env perder um valor que estava setado no shell antes do comando.

Ler e escrever com colchetes

process.env.DB_NAME e process.env['DB_NAME'] devolvem a mesma coisa. Para

escrever, o colchete não é preciosismo: é o que o dotenv faz, e a diferença

aparece quando o nome da variável colide com palavra do JavaScript.

process.env.NODE_ENV = 'production';        // ok
process.env['NODE_ENV'] = 'production';     // ok tambem

Depois do ponto só pode vir um identificador válido; entre colchetes pode vir

qualquer nome. É por isso que um .env com valor undefined, null ou NaN

chega ao processo como string vazia — o dotenv não escreve esses valores.

Exemplo

'use strict';

// Exemplo da aula 2 do dia 2: variavel de ambiente e o arquivo `.env`.
//
// O ponto que a aula precisa deixar claro: a senha NAO esta neste arquivo.
// O que esta aqui e o NOME da variavel e o mecanismo que a preenche. O valor
// vive no `.env`, que o `.gitignore` exclui — e a saida deste exemplo vira
// pagina publica, entao ela nunca imprime valor de senha, so se chegou.

const mysql = require('mysql2/promise');

async function main() {
  // `process.env` e um objeto comum do Node: cada variavel de ambiente e uma
  // chave. `NODE_ENV` e a convencao do ecosystema Node — 'production' em deploy,
  // 'development' (ou ausente) no resto.
  console.log('NODE_ENV:', process.env.NODE_ENV || '(nao definida)');

  // O que o arquivo `.env` trouxe para dentro do processo. Estes nomes sao
  // publicos por convencao e os valores de configuracao nao tem nada de secreto.
  console.log('');
  console.log('variaveis do ambiente, por nome:');
  for (const nome of ['DB_HOST', 'DB_PORT', 'DB_USER', 'DB_NAME']) {
    console.log('  ' + nome + ' = ' + (process.env[nome] || '(vazia)'));
  }

  // A senha e o caso oposto: o exemplo so diz se ela chegou. Imprimir o valor
  // aqui seria o mesmo que comitar a senha.
  console.log('');
  console.log('DB_PASS preenchida?', process.env.DB_PASS ? 'sim, e o valor NAO sai daqui' : 'nao');

  // --- a prova de que o `.env` chegou: o banco aceita a conexao ---
  //
  // O `dotenv` nao e chamado neste arquivo de proposito. Num programa de verdade
  // quem le o `.env` e o `require('dotenv').config()` que roda na primeira
  // linha, antes de qualquer outra coisa; e por isso que `process.env.DB_PASS`
  // ja esta preenchida aqui. Repetir a chamada em cada arquivo seria ruido que
  // o aluno copiaria sem necessidade.
  const conexao = await mysql.createConnection({
    host: process.env.DB_HOST,
    port: Number(process.env.DB_PORT),
    user: process.env.DB_USER,
    password: process.env.DB_PASS,
    database: process.env.DB_NAME,
  });

  const [ambiente] = await conexao.query('SELECT DATABASE() AS banco, USER() AS usuario');
  console.log('');
  console.log('o .env montou uma conexao de verdade:');
  console.log('  banco:', ambiente[0].banco, '| usuario:', ambiente[0].usuario);
  await conexao.end();
  console.log('  (a senha viajou de process.env para o driver e nao para a tela)');

  // A variavel de ambiente existe para mudar o comportamento do mesmo codigo sem
  // mudar o codigo. `DB_NAME` aponta para o banco de teste aqui e para outro
  // banco no deploy, e nenhum dos dois precisa ser reescrito.
  console.log('');
  console.log('--- o mesmo codigo, dois ambientes ---');
  const ambienteAtual = process.env.NODE_ENV || 'development';
  if (ambienteAtual === 'production') {
    console.log('  NODE_ENV=production: log detalhado desligado, so o essencial');
  } else {
    console.log('  NODE_ENV=' + ambienteAtual + ': log detalhado ligado');
  }

  // `process.env` aceita escrita, e o valor novo vale so neste processo e nos
  // filhos que ele criar. Nao ha como mudar o ambiente de outro processo ja
  // rodando, e o valor nao sobrevive ao fim do processo.
  //
  // A chave vai entre colchetes, e nao depois de um ponto, por um motivo que
  // importa mais do que parece: o `dotenv` faz exatamente assim, e e por isso
  // que ele nao consegue ler um `.env` cujo valor seja uma palavra do JavaScript
  // (`undefined`, `NaN`, `null` viram string vazia). Depois do ponto so pode vir
  // um identificador valido; entre colchetes pode vir qualquer nome.
  const antes = process.env['MARCADOR_DEMO'];
  process.env['MARCADOR_DEMO'] = 'definido pelo proprio processo';
  console.log('');
  console.log('MARCADOR_DEMO antes:', antes || '(nao existia)');
  console.log('MARCADOR_DEMO agora:', process.env['MARCADOR_DEMO']);

  // Os dois arquivos e o que cada um guarda. O modelo vai para o git; o valor de
  // verdade, nao.
  console.log('');
  console.log('env.exemplo  -> git     (nome da variavel + valor de exemplo)');
  console.log('.env         -> sem git  (valor de verdade)');
  console.log('.gitignore   -> sem git  (e lista .env, node_modules/ e *.log)');
  console.log('process.env  -> so memoria do processo, nunca em disco');
}

main().catch((erro) => {
  console.error('falhou:', erro.code || erro.name, '-', erro.message);
  process.exit(1);
});

Saída real

NODE_ENV: (nao definida)

variaveis do ambiente, por nome:
  DB_HOST = 127.0.0.1
  DB_PORT = 3306
  DB_USER = materiais
  DB_NAME = materiais_teste

DB_PASS preenchida? sim, e o valor NAO sai daqui

o .env montou uma conexao de verdade:
  banco: materiais_teste | usuario: materiais@localhost
  (a senha viajou de process.env para o driver e nao para a tela)

--- o mesmo codigo, dois ambientes ---
  NODE_ENV=development: log detalhado ligado

MARCADOR_DEMO antes: (nao existia)
MARCADOR_DEMO agora: definido pelo proprio processo

env.exemplo  -> git     (nome da variavel + valor de exemplo)
.env         -> sem git  (valor de verdade)
.gitignore   -> sem git  (e lista .env, node_modules/ e *.log)
process.env  -> so memoria do processo, nunca em disco