Dia 15 — Fechamento do ano letivo

Informatica · Conteudo · publicado em 30/09/2026
Dia 15 de 15

Fechamento do ano letivo

Aula 1

Revisão do ano: do navegador ao servidor

A mesma linguagem, outro lugar

Esta é a revisão do ano: o que o JavaScript no navegador e o JavaScript no servidor têm em comum, o que não têm, e por que a passagem do primeiro para o segundo trimestre custou mais do que a troca de sintaxe.

O JavaScript do navegador e o JavaScript do servidor são o mesmo interpretador, a mesma linguagem e as mesmas palavras reservadas. O que muda é o que existe em volta: no navegador existe uma janela, um documento e alguém esperando; no servidor existe um processo, uma porta e ninguém esperando.

Por isso a transição do ano não foi de sintaxe. const, let, template literal, desestruturação e arrow function não mudaram de lugar. O que mudou foi o que o código precisa fazer para provocar alguma coisa.

no navegadorno servidor
o que o código éum arquivo carregado pela páginaum processo que fica rodando
o objeto globaldocumentprocess
quem chama o códigoa pessoa, clicandoa rede, mandando requisição
como o código recebe o pedidoo target do eventoo req, objeto da requisição
como o código respondeescrevendo no DOMres.status e res.end()
onde o dado ficalocalStorage, textotabela no MySQL, com coluna e tipo
o que acontece quando acabaa aba fecha e o dado someo processo morre e o dado fica
quem dá o tempoo evento do usuárioo event loop, e só ele

A última linha é a que mais importa. No navegador o tempo vem de fora: o evento acontece e o código roda. No servidor não existe ninguém esperando, então o código precisa criar o próprio tempo, com await, com listen, com setInterval. Um laço sem await dentro trava o processo inteiro, e a requisição que chegou atrás dele nunca é atendida.

O que mudou de um lado para o outro

Cada linha abaixo é uma troca de conceito, não de palavra. A coluna da direita é o que ocupa o lugar da esquerda quando o código sai da aba.

no navegadorno servidoro que a troca exige
document.querySelectorreq.urla rota decide o que fazer, não o caminho de um arquivo
addEventListener('click', fn)servidor.on('listening', fn)o evento passa a ser do processo, não do botão
evento de keydownservidor.on('request', fn)o que chega é requisição, com método e com corpo
localStorage.setItemINSERT INTO ... VALUES (?, ?)o dado ganha coluna, tipo e chave primária
localStorage.getItemSELECT ... WHERE id = ?a leitura volta como linha, e não como texto
fetch('/outra-pagina.html')fetch('/rota')o alvo passa a ser a própria API, e a resposta é JSON
JSON.parse e JSON.stringifyos mesmos doisJSON é o contrato, e é o que não mudou

localStorage é a linha mais enganosa da tabela, porque parece um banco e não é: guarda texto, não tem tipo, não tem consulta, e some quando o navegador decide limpar. Trocar localStorage por MySQL não é trocar de método, é trocar de categoria. O que era uma atribuição vira uma consulta, e o que era síncrono vira await.

document não tem substituto direto. No servidor o que ocupa o lugar do documento é o par req e res: um descreve o que chegou, o outro é a única forma de responder. Quem escreve a rota segura os dois e decide qual status mandar.

E o fetch muda de direção. No navegador ele apontava para fora, para outra página ou outro serviço; no servidor aponta para dentro, para a rota do próprio processo. É a mesma função com outro destino — e é por isso que async e await não precisaram mudar de lugar.

O que não mudou

A lista curta é a que mais rende, porque é o que já está pronto:

  • A sintaxe. const, template literal, desestruturação, spread, arrow function.
  • async e await. A forma de escrever a espera é a mesma nos dois lados. Muda o que se espera: no navegador, uma resposta; no servidor, uma consulta ao MySQL.
  • try e catch. O bloco é idêntico. Mudou o que se compara dentro dele: erro.code diz o que aconteceu, e erro.message do MySQL vem em inglês e muda entre versões.
  • JSON. Serializar e desserializar é o mesmo código nos dois lugares. É o motivo de o formato ter atravessado o ano sem ser reescrito.
  • O modelo de promessa. then, catch, finally e o encadeamento com await são idênticos.

Dúvida sobre sintaxe ou sobre await já foi resolvida: o que foi aprendido no primeiro trimestre continua valendo palavra por palavra.

O mapa completo do ano

O caminho do front ao back cabe em três blocos, e o exemplo da aula roda o primeiro e o segundo lado a lado.

blocoonde o código rodavao que era
1º trimestredentro da abasintaxe, função, async/await, evento, DOM
2º trimestrenum processorequire, rota HTTP, req e res, MySQL, consulta parametrizada
3º trimestrenum processo que precisa sobrevivercamadas, autenticação, teste, migração, validação, segurança, índice, transação, deploy, log

O que foi aprendido em cada bloco, e o que fica para trás: o que não cabia no anterior. Uma API que funciona e não tem teste é o resultado honesto de um curso de sintaxe; uma API com teste, migração e deploy é o resultado de um curso que teve de pensar em quem vai manter aquilo depois.

O que ficou para trás no fim do ano é curto e conhecido: a porta de produção nunca foi aberta de verdade. O processo roda com pm2 na máquina de quem estuda, o banco está na mesma máquina, e nenhum outro computador falou com esse servidor.

O toolkit: os comandos que voltam

Cada comando resolve um problema específico. Comando sem motivo definido é o que se decora e esquece.

comandoo que resolve
node --check arquivo.jsdizer se o arquivo tem erro de sintaxe, sem executar nada
node -r ./_com_mysql.js exemplo.jscarregar o harness, que lê o .env e abre a conexão
.env com process.envtirar a credencial do arquivo de código e tirá-la do git
node --testrodar o teste e dizer o que passou e o que quebrou
EXPLAIN SELECT ...mostrar o plano da consulta e denunciar a tabela sem índice
CREATE INDEX ...criar o índice que o EXPLAIN mostrou que faltava
BEGIN e COMMIT e ROLLBACKagrupar as escritas, ou desfazer o que não devia ter gravado
pm2 start servidor.jsfazer o processo voltar sozinho quando ele cai
docker compose upsubir aplicação e banco juntos, em qualquer máquina
git log --onelineguardar o histórico e voltar atrás de uma mudança

O exemplo da aula roda node --check sobre ele mesmo e consulta quatro binários com --version, o que dá a resposta da máquina que executou — e a da sua máquina pode ser outra. A porta muda do mesmo jeito: é o listen(0) pedindo uma livre ao sistema, e é por isso que o número muda entre uma rodada e outra sem que o exemplo esteja errado.

O que você já sabe fazer

Escrito no presente, porque é o que está pronto para uso:

  • Você lê um valor de process.env e sabe que o valor da senha não entra no arquivo de código.
  • Você escolhe a porta com listen(0), espera o evento 'listening' antes de chamar, e fecha com close() no finally.
  • Você monta uma rota que decide o status e não sabe SQL nenhum.
  • Você grava com ? parametrizado e não concatena valor de fora da aplicação.
  • Você decide o que fazer comparando erro.code, e não a message.
  • Você separa rota, serviço e repositório, e sabe por quê.
  • Você roda o mesmo exemplo duas vezes e o resultado é o mesmo, porque o TRUNCATE está no começo.

O TRUNCATE no começo e não no fim é detalhe pequeno que evita defeito grande: com DELETE o AUTO_INCREMENT continua crescendo, a saída embutida muda a cada execução, e a página passa a mentir sobre o que o exemplo imprime.

A tabela do mapa é a peça que vale reler quando a dúvida for "onde isso eu vi". Cada linha dela é um conceito já resolvido, e o que não está na tabela é o que ainda falta aprender.

O exemplo da aula roda as duas metades e compara as três leituras — a da memória, a do SQL direto e a do fetch para a própria API. A igualdade sai de JSON.stringify sobre as listas, não de uma frase, e é o mesmo mecanismo que faz a página e a API concordarem sobre o formato do dado.

Exemplo

'use strict';

// Exemplo da aula 1 do dia 15: o ano inteiro em uma página — a MESMA lista de
// itens lida pelas duas metades, a do navegador e a do servidor.
//
// A promessa do exemplo é a mesma da aula: uma lista de três itens é guardada
// na memória (o que o `localStorage` fazia), depois gravada no MySQL, servida
// por uma rota e lida de volta com `fetch` para a própria API. O exemplo
// compara as duas leituras e imprime se elas são iguais — a igualdade vem de
// `JSON.stringify` sobre as duas listas, não de uma frase.
//
// Quatro medições reais sustentam o mapa do caminho:
//   1. `servidor.listening` é `false` antes do `listen` e `true` depois
//   2. a lista da memória e a lista da API passam pelo mesmo comparador
//   3. `node --check` roda de verdade sobre este arquivo
//   4. `erro.code` de uma consulta que falha vem impresso, e o `stderr` fica no terminal
//
// A porta muda a cada execução: é o `listen(0)` pedindo uma porta livre ao
// sistema. A página mostra um número e a execução seguinte mostra outro.

const http = require('node:http');
const { execFileSync } = require('node:child_process');
const { createConnection } = require('mysql2/promise');

// ============================================= 1. a metade do navegador
// A memória do navegador guardava a lista entre uma página e outra. Aqui ela é
// um `Map` comum: sem processo, sem porta, sem banco. E some quando o processo
// termina — que é o motivo de a mesma lista precisar virar linha no MySQL.
function criarMemoriaDoNavegador() {
  const dados = new Map();
  return {
    // O `localStorage` guardava TEXTO. Por isso o `JSON.stringify` aqui e o
    // `JSON.parse` na leitura: é o mesmo formato, com o mesmo motivo.
    gravar(chave, valor) {
      dados.set(chave, JSON.stringify(valor));
    },
    ler(chave) {
      const bruto = dados.get(chave);
      return bruto === undefined ? null : JSON.parse(bruto);
    },
    apagar(chave) {
      dados.delete(chave);
    },
    chaves() {
      return [...dados.keys()];
    },
  };
}

// ================================================= 2. o mapa do caminho
// Cada linha é uma troca de CONCEITO, não de palavra. O que muda de um lado
// para o outro muda porque o lugar do código muda: a aba vira processo, o
// botão vira rota, o texto guardado vira coluna.
const TROCOU = [
  ['onde o código roda', 'dentro da aba, com alguém esperando', 'num processo Node, escutando uma porta'],
  ['o objeto global', '`document`', '`process`, mais o `req` e o `res` de cada requisição'],
  ['o evento que dispara tudo', "`click` no botão", "o evento `'listening'` e o callback da rota"],
  ['de onde o dado entra', 'o que o usuário digitou', 'o `process.env` e o corpo JSON da requisição'],
  ['onde o dado fica', '`localStorage`, texto no navegador', 'tabela no MySQL, com coluna e tipo'],
  ['o `fetch`', 'para outra página, outra origem', 'para a própria API, no mesmo processo'],
];

// O que não mudou. Não é enfeite: é a lista do que o aluno já
// sabe usar e não vai reaprender.
const FICOU = [
  ['a sintaxe', '`const`, template literal, desestruturação'],
  ['`async` e `await`', 'a forma de escrever a espera'],
  ['`try` e `catch`', 'a forma de tratar o erro, e `erro.code` para decidir'],
  ['JSON', 'o formato que atravessa a rede e chega igual dos dois lados'],
  ['a promessa', 'o objeto que carrega o valor depois'],
];

// ================================================ 3. o toolkit, comando a comando
// Cada linha é um comando que o aluno vai voltar a usar. A coluna `onde` diz o
// que ele resolve — porque "comando sem motivo" é o que o aluno decora e esquece.
const FERRAMENTAS = [
  ['node --check arquivo.js', 'diz se o arquivo tem erro de sintaxe, sem executar nada'],
  ['node -r ./_com_mysql.js exemplo.js', 'carrega o harness, que lê o `.env` e abre a conexão'],
  ['.env + `process.env`', 'tira a credencial do arquivo de código e a deixa fora do git'],
  ['node --test', 'roda o teste do arquivo e diz o que passou e o que quebrou'],
  ['EXPLAIN SELECT ...', 'mostra o plano da consulta e denuncia a tabela sem índice'],
  ['CREATE INDEX ...', 'cria o índice que o `EXPLAIN` mostrou que faltava'],
  ['BEGIN / COMMIT / ROLLBACK', 'agrupa as escritas, ou desfaz o que não devia ter gravado'],
  ['pm2 start servidor.js', 'faz o processo voltar sozinho quando ele cai'],
  ['docker compose up', 'sobe a aplicação e o banco juntos, em qualquer máquina'],
  ['git log --oneline', 'guarda o histórico e deixa voltar atrás de uma mudança'],
];

// O binário que existe na máquina que rodou o exemplo. A linha muda de máquina
// para máquina — é uma medição do ambiente, não uma afirmação sobre o seu.
const BINARIOS = [
  ['git', 'git'],
  ['docker', 'docker'],
  ['npm', 'npm'],
  ['pm2', 'pm2'],
];

// `execFileSync` com `timeout`: um binário que trava não pode segurar o exemplo
// até o limite do portão. O `catch` devolve "não instalado", que é a resposta
// honesta quando o comando não existe.
function estadoDoBinario(nome) {
  try {
    execFileSync(nome, ['--version'], {
      stdio: ['ignore', 'pipe', 'ignore'],
      timeout: 4000,
    });
    return 'instalado nesta máquina';
  } catch {
    return 'não instalado nesta máquina';
  }
}

// `DECIMAL` volta do driver como texto: `'12.50'`, e não `12.5`. O comparador
// das duas metades converte com `Number` antes de comparar, senão a igualdade
// seria uma mentira de tipo.
const normaliza = (item) => ({
  nm_item: item.nm_item,
  vl_preco: Number(item.vl_preco),
});

async function main() {
  const c = await 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,
    multipleStatements: true,
  });

  // ============================================================ o esquema
  await c.query(`
    CREATE TABLE IF NOT EXISTS tb_d15a1_revisao (
      id        INT AUTO_INCREMENT PRIMARY KEY,
      nm_item   VARCHAR(40) NOT NULL,
      vl_preco  DECIMAL(10,2) NOT NULL,
      cd_origem VARCHAR(10) NOT NULL
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
  `);

  // `TRUNCATE` no COMEÇO, e não no fim: a tabela existe com o estado que a
  // execução anterior deixou, e o exemplo recomeça sempre igual. O
  // `AUTO_INCREMENT` volta a 1, e é por isso que os `id` não crescem na saída.
  await c.query('TRUNCATE TABLE tb_d15a1_revisao');

  // ================================================ 1. a metade do navegador
  console.log('--- 1. no navegador: um objeto em memória ---');

  const memoria = criarMemoriaDoNavegador();
  memoria.gravar('carrinho', [
    { nm_item: 'teclado', vl_preco: 189.9 },
    { nm_item: 'mouse', vl_preco: 79.5 },
    { nm_item: 'monitor', vl_preco: 1299 },
  ]);

  const listaDaMemoria = memoria.ler('carrinho');
  console.log('chaves guardadas: ' + memoria.chaves().join(', '));
  console.log('itens na memória: ' + listaDaMemoria.length);
  for (const item of listaDaMemoria) {
    console.log('  ' + item.nm_item.padEnd(10) + ' R$ ' + item.vl_preco);
  }
  console.log('o formato guardado é TEXTO: '
    + JSON.stringify(JSON.parse(JSON.stringify(listaDaMemoria))).slice(0, 34) + '...');
  console.log('nenhum processo subiu, nenhuma porta abriu, nenhum banco foi tocado');
  console.log('mas a lista morre com o processo — e é por isso que ela vira linha');

  // ================================================= 2. a metade do servidor
  console.log('\n--- 2. no servidor: processo, porta, rota e banco ---');

  const servidor = http.createServer(async (req, res) => {
    const responder = (status, corpo) => {
      res.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8' });
      res.end(JSON.stringify(corpo));
    };

    const [caminho] = req.url.split('?');

    if (req.method === 'GET' && caminho === '/item') {
      // Rota com SQL dentro: é o estado do 1º trimestre. Funciona, e é
      // exatamente por funcionar que a camada de serviço do 3º trimestre
      // existe. O `?` parametrizado já está aqui desde o dia 13 do 2º.
      const [linhas] = await c.query(
        'SELECT nm_item, vl_preco FROM tb_d15a1_revisao ORDER BY id'
      );
      return responder(200, { itens: linhas });
    }

    if (req.method === 'POST' && caminho === '/item') {
      // O corpo da requisição é a entrada do cliente: entra por `?`, nunca por
      // concatenação.
      const partes = [];
      for await (const p of req) partes.push(p);
      const corpo = JSON.parse(Buffer.concat(partes).toString('utf8') || '{}');
      const [r] = await c.execute(
        'INSERT INTO tb_d15a1_revisao (nm_item, vl_preco, cd_origem) VALUES (?, ?, ?)',
        [String(corpo.nm_item), Number(corpo.vl_preco), String(corpo.cd_origem || 'servidor')]
      );
      return responder(201, { id: r.insertId });
    }

    return responder(404, { erro: 'rota não encontrada' });
  });

  // Medição antes de ouvir: o processo existe e não atende ninguém. É a
  // diferença real entre "escrevi um arquivo" e "subi um serviço".
  console.log('servidor criado, `listening` = ' + servidor.listening);

  await new Promise((r) => servidor.listen(0, '127.0.0.1', r));
  const base = 'http://127.0.0.1:' + servidor.address().port;
  console.log('`listen(0)` devolveu a porta ' + servidor.address().port
    + ' e `listening` = ' + servidor.listening);
  console.log('a porta muda a cada execução: o sistema escolhe uma livre');

  // Duas medidas que a página usa depois do `try`: quanta linha entrou por
  // parâmetro e se as três leituras batem. Declaradas aqui porque o `const`
  // dentro do `try` morre com o bloco.
  let porParametro = 0;
  let iguais = false;
  let codigoDoErro = '(nenhum)';

  try {
    // A lista da memória vira linha. Cada item sai daqui com `cd_origem`
    // marcando de qual metade ele veio — a prova de que os dois caminhos
    // alimentam a mesma tabela, e não duas tabelas parecidas.
    for (const item of listaDaMemoria) {
      const resposta = await fetch(base + '/item', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ ...item, cd_origem: 'navegador' }),
      });
      const criado = await resposta.json();
      console.log('  POST /item  ' + String(resposta.status).padEnd(4)
        + ' id ' + criado.id + '  ' + item.nm_item + ' (memória -> MySQL)');
    }

    // O `id` foi lido de volta da resposta, não calculado de cabeça: o
    // `AUTO_INCREMENT` quem devolve é o banco.
    const [gravadas] = await c.query(
      'SELECT COUNT(*) AS n FROM tb_d15a1_revisao WHERE cd_origem = ?', ['navegador']);
    porParametro = gravadas[0].n;
    console.log('linhas gravadas com cd_origem = "navegador": ' + porParametro);
    console.log('o que guardava texto agora tem coluna, tipo e chave primária');

    // ============================ 3. a mesma lista, pelas duas metades
    console.log('\n--- 3. a mesma lista lida pelas duas vias ---');

    const [direto] = await c.query(
      'SELECT nm_item, vl_preco FROM tb_d15a1_revisao ORDER BY id'
    );
    const respostaApi = await fetch(base + '/item');
    const viaApi = (await respostaApi.json()).itens;

    const memoriaNorm = listaDaMemoria.map(normaliza);
    const bancoNorm = direto.map(normaliza);
    const apiNorm = viaApi.map(normaliza);

    console.log('status do GET /item: ' + respostaApi.status);
    console.log('memória  -> ' + JSON.stringify(memoriaNorm));
    console.log('SQL direto-> ' + JSON.stringify(bancoNorm));
    console.log('fetch API-> ' + JSON.stringify(apiNorm));

    iguais = JSON.stringify(memoriaNorm) === JSON.stringify(apiNorm)
      && JSON.stringify(bancoNorm) === JSON.stringify(apiNorm);
    console.log('as três leituras são iguais: ' + iguais);
    console.log('mesmo conteúdo, mesmo formato, mesma ordem: JSON é o contrato');

    // ======================== 4. o que mudou, o que ficou, o que dá erro
    console.log('\n--- 4. o mapa do caminho ---');
    for (const [conceito, antes, agora] of TROCOU) {
      console.log('  ' + (conceito + ':').padEnd(28) + antes.padEnd(32) + ' -> ' + agora);
    }
    console.log('');
    for (const [conceito, como] of FICOU) {
      console.log('  ' + ('ficou: ' + conceito + ' —').padEnd(30) + como);
    }

    // O `try`/`catch` não mudou: o que mudou foi o que se compara dentro dele.
    // O `erro.code` diz o que aconteceu; a `message` do MySQL vem em inglês e
    // não serve para decidir nada. O par de linhas abaixo é obrigatório: o
    // `console.error` fica no terminal e o `console.log` chega na página.
    console.log('\n--- 5. o `try`/`catch` que não mudou, e o `erro.code` que sim ---');
    console.log('consultando uma tabela que não existe, de propósito:');
    try {
      await c.query('SELECT id FROM tb_que_nao_existe_no_material');
      console.log('  nao deveria chegar aqui');
    } catch (erro) {
      console.error(erro.code + ': ' + erro.message);   // o terminal
      codigoDoErro = erro.code;
      console.log('  erro.code   : ' + erro.code);
      console.log('  erro.sqlState: ' + erro.sqlState);
      console.log('  a mensagem vem em inglês e muda entre versões: o `code` é o que decide');
    }
  } finally {
    await new Promise((r) => servidor.close(r));
    console.log('\nservidor encerrado com `close()` no `finally`.');
  }

  // ==================================================== 6. o toolkit
  console.log('\n--- 6. o toolkit, um comando por vez ---');
  for (const [comando, resolve] of FERRAMENTAS) {
    console.log('  ' + comando.padEnd(38) + resolve);
  }
  console.log('');
  for (const [rotulo, binario] of BINARIOS) {
    console.log('  ' + (rotulo + ' --version').padEnd(22) + estadoDoBinario(binario));
  }
  console.log('cada linha acima saiu de rodar o binário agora; na sua máquina pode diferir');

  // O `node --check` roda de verdade sobre ESTE arquivo, e o código de saída
  // diz se passou. Se um dia alguém quebrar a sintaxe aqui, esta linha muda.
  let check;
  try {
    execFileSync(process.execPath, ['--check', __filename], { stdio: 'pipe' });
    check = 'sem erro de sintaxe';
  } catch {
    check = 'ERRO DE SINTAXE — e o exemplo não deveria ter subido assim';
  }
  console.log('node --check neste arquivo: ' + check);

  // =========================================== 7. o que já dá para fazer
  // Escrito na segunda pessoa e no presente, porque é a lista do que o aluno
  // vai procurar quando estiver escrevendo o próximo código.
  console.log('\n--- 7. o que você já sabe fazer ---');
  console.log('  você lê um valor de `process.env`: DB_HOST está '
    + (process.env.DB_HOST ? 'preenchido' : 'vazio')
    + ', e o valor da senha nunca sai daqui');
  console.log("  você escolhe a porta com `listen(0)`, espera o evento `'listening'` e fecha com `close()`");
  console.log('  você monta uma rota que decide o `status` e não sabe SQL');
  console.log('  você grava com `?` e nunca concatena valor de fora: '
    + porParametro + ' linha(s) entraram por parâmetro');
  console.log('  você decide pelo `erro.code`, e o que apareceu foi ' + codigoDoErro);
  console.log('  você compara JSON das duas metades e a igualdade é '
    + iguais + ', sem ninguém afirmar que é');
  console.log('  você roda o exemplo duas vezes e o resultado é o mesmo, porque o `TRUNCATE` é no começo');

  // O `TRUNCATE` do início se prova aqui: a tabela tem exatamente três linhas
  // agora, e terá três na próxima execução.
  const [final] = await c.query('SELECT COUNT(*) AS n FROM tb_d15a1_revisao');
  console.log('linhas na tabela ao fim desta execução: ' + final[0].n
    + ' (o TRUNCATE do começo zera tudo)');

  const [versao] = await c.query('SELECT VERSION() AS versao');
  console.log('\nbanco em uso: ' + versao[0].versao);
  console.log('Node em uso: ' + process.version);
  console.log('as duas versões acima foram capturadas do banco e do runtime, não escritas à mão');

  await c.end();
}

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

Saída real

--- 1. no navegador: um objeto em memória ---
chaves guardadas: carrinho
itens na memória: 3
  teclado    R$ 189.9
  mouse      R$ 79.5
  monitor    R$ 1299
o formato guardado é TEXTO: [{"nm_item":"teclado","vl_preco":1...
nenhum processo subiu, nenhuma porta abriu, nenhum banco foi tocado
mas a lista morre com o processo — e é por isso que ela vira linha

--- 2. no servidor: processo, porta, rota e banco ---
servidor criado, `listening` = false
`listen(0)` devolveu a porta 39587 e `listening` = true
a porta muda a cada execução: o sistema escolhe uma livre
  POST /item  201  id 1  teclado (memória -> MySQL)
  POST /item  201  id 2  mouse (memória -> MySQL)
  POST /item  201  id 3  monitor (memória -> MySQL)
linhas gravadas com cd_origem = "navegador": 3
o que guardava texto agora tem coluna, tipo e chave primária

--- 3. a mesma lista lida pelas duas vias ---
status do GET /item: 200
memória  -> [{"nm_item":"teclado","vl_preco":189.9},{"nm_item":"mouse","vl_preco":79.5},{"nm_item":"monitor","vl_preco":1299}]
SQL direto-> [{"nm_item":"teclado","vl_preco":189.9},{"nm_item":"mouse","vl_preco":79.5},{"nm_item":"monitor","vl_preco":1299}]
fetch API-> [{"nm_item":"teclado","vl_preco":189.9},{"nm_item":"mouse","vl_preco":79.5},{"nm_item":"monitor","vl_preco":1299}]
as três leituras são iguais: true
mesmo conteúdo, mesmo formato, mesma ordem: JSON é o contrato

--- 4. o mapa do caminho ---
  onde o código roda:         dentro da aba, com alguém esperando -> num processo Node, escutando uma porta
  o objeto global:            `document`                       -> `process`, mais o `req` e o `res` de cada requisição
  o evento que dispara tudo:  `click` no botão                 -> o evento `'listening'` e o callback da rota
  de onde o dado entra:       o que o usuário digitou          -> o `process.env` e o corpo JSON da requisição
  onde o dado fica:           `localStorage`, texto no navegador -> tabela no MySQL, com coluna e tipo
  o `fetch`:                  para outra página, outra origem  -> para a própria API, no mesmo processo

  ficou: a sintaxe —            `const`, template literal, desestruturação
  ficou: `async` e `await` —    a forma de escrever a espera
  ficou: `try` e `catch` —      a forma de tratar o erro, e `erro.code` para decidir
  ficou: JSON —                 o formato que atravessa a rede e chega igual dos dois lados
  ficou: a promessa —           o objeto que carrega o valor depois

--- 5. o `try`/`catch` que não mudou, e o `erro.code` que sim ---
consultando uma tabela que não existe, de propósito:
  erro.code   : ER_NO_SUCH_TABLE
  erro.sqlState: 42S02
  a mensagem vem em inglês e muda entre versões: o `code` é o que decide

servidor encerrado com `close()` no `finally`.

--- 6. o toolkit, um comando por vez ---
  node --check arquivo.js               diz se o arquivo tem erro de sintaxe, sem executar nada
  node -r ./_com_mysql.js exemplo.js    carrega o harness, que lê o `.env` e abre a conexão
  .env + `process.env`                  tira a credencial do arquivo de código e a deixa fora do git
  node --test                           roda o teste do arquivo e diz o que passou e o que quebrou
  EXPLAIN SELECT ...                    mostra o plano da consulta e denuncia a tabela sem índice
  CREATE INDEX ...                      cria o índice que o `EXPLAIN` mostrou que faltava
  BEGIN / COMMIT / ROLLBACK             agrupa as escritas, ou desfaz o que não devia ter gravado
  pm2 start servidor.js                 faz o processo voltar sozinho quando ele cai
  docker compose up                     sobe a aplicação e o banco juntos, em qualquer máquina
  git log --oneline                     guarda o histórico e deixa voltar atrás de uma mudança

  git --version         instalado nesta máquina
  docker --version      instalado nesta máquina
  npm --version         instalado nesta máquina
  pm2 --version         não instalado nesta máquina
cada linha acima saiu de rodar o binário agora; na sua máquina pode diferir
node --check neste arquivo: sem erro de sintaxe

--- 7. o que você já sabe fazer ---
  você lê um valor de `process.env`: DB_HOST está preenchido, e o valor da senha nunca sai daqui
  você escolhe a porta com `listen(0)`, espera o evento `'listening'` e fecha com `close()`
  você monta uma rota que decide o `status` e não sabe SQL
  você grava com `?` e nunca concatena valor de fora: 3 linha(s) entraram por parâmetro
  você decide pelo `erro.code`, e o que apareceu foi ER_NO_SUCH_TABLE
  você compara JSON das duas metades e a igualdade é true, sem ninguém afirmar que é
  você roda o exemplo duas vezes e o resultado é o mesmo, porque o `TRUNCATE` é no começo
linhas na tabela ao fim desta execução: 3 (o TRUNCATE do começo zera tudo)

banco em uso: 10.11.14-MariaDB-0ubuntu0.24.04.1
Node em uso: v24.21.0
as duas versões acima foram capturadas do banco e do runtime, não escritas à mão
Aula 2

Para onde seguir

O que este curso terminou e o que não terminou

O que terminou: escrever um servidor HTTP em Node.js que grava em MySQL, protege com token, valida entrada, tem teste, tem migração, tem índice, tem log e sobe em container.

O que não terminou: abrir esse servidor para outra pessoa usar de verdade. O processo roda na sua máquina, o banco roda na sua máquina, e nenhum outro computador falou com ele. Um domínio apontando para lá, um certificado, um segundo servidor para quando o primeiro cair — nada disso foi feito.

Essas duas frases são o resumo honesto do ano. A segunda não é falta do curso: é a fronteira do que cabe em um curso de-fundamentos. Saber escrever o servidor é o primeiro degrau; fazê-lo existir fora da sua máquina é o trabalho que vem depois, e ele depende de coisas que só aparecem com servidor de verdade.

Um cuidado honesto sobre o conjunto do ano: o título "curso de Node.js e MySQL" não é certificado, e nada aqui garante emprego. O que o ano produziu é a base técnica para seguir estudando. Quem continua, continua; quem para aqui tem uma base real, e a base real é o que faz o próximo passo ser possível.

Os quatro caminhos

O que vem depois se organiza em quatro blocos. Nenhum é obrigatório, e a ordem importa mais do que a escolha.

1. Mais fundo em Node.js

O event loop é a peça central, e é a única parte do Node que precisa ser entendida em vez de decorada. A regra é curta: toda operação que prende o event loop atrasa todas as requisições, e não só a dela. Um while sem await, um JSON.parse de um arquivo de 200 MB, uma consulta sem índice — todos seguram o processo inteiro.

O que vem depois disso:

  • stream — ler e escrever arquivo sem carregar tudo na memória.
  • worker_threads — tirar o trabalho pesado do event loop para outra thread.
  • cluster — vários processos na mesma máquina, dividindo a porta.
  • async_hooks — enxergar o que está pendente e o que está segurando o loop.

2. O outro lado do banco

Você já sabe criar índice e ler EXPLAIN. O que falta é a escala, e os assuntos são estes:

  • Índice composto com a ordem das colunas de propósito, e o motivo de a ordem importar.
  • EXPLAIN de verdade — key_len, rows, Extra, e não só "usou índice ou não".
  • **SELECT *** como hábito caro — ele traz coluna que ninguém pediu e impede o índice de cobertura, em que a resposta sai do índice sem tocar na tabela.
  • Replicação — a réplica recebe uma cópia do que o principal gravou.
  • READ REPLICA — ler da réplica em vez do principal, e o preço: a réplica atrasa, e leitura atrasada parece bug.

3. A camada de fora

O protocolo e os serviços em volta do servidor:

  • HTTP/2 — multiplexação e compressão no mesmo protocolo.
  • WebSocket — conexão que fica aberta e empurra dados, em vez de esperar o cliente perguntar.
  • GraphQL — o cliente pede os campos de que precisa em uma requisição só.
  • Redis — cache na frente do banco, para o que é lido muito e muda pouco.
  • Fila de verdade — RabbitMQ ou Amazon SQS, com consumidor, retentativa e aviso de mensagem perdida. A tabela de jobs do 3º trimestre é o rascunho disso, e a diferença é que fila de verdade sobrevive a queda do processo.
  • TypeScript — o JavaScript com tipo escrito no código, que faz o editor recusar o acesso a campo que não existe antes de o programa rodar. Continuar estudando em JavaScript puro faz sentido; mudar de linguagem é o passo seguinte, não o atual.
  • Framework do servidor — Express cobre o que os exemplos deste curso fizeram na mão. Serve quando a rota começa a repetir validação, try/catch e cabeçalho em todo lugar.
  • React — é interface, do outro lado da API. Serve quando o objetivo passa a ser a tela; não serve para consertar servidor lento.
  • API pública — a mesma API que roda na sua máquina, com domínio, HTTPS e limite de taxa, exposta para fora. É o ponto onde a aula 1 deste dia vira projeto real.
  • Banco no deploy — o banco na mesma máquina da aplicação, ou gerenciado fora dela. Muda o que se perde: com o banco na máquina, perder a máquina é perder o dado.

4. O ofício

Este é o caminho que mais muda o salário e o que o curso mais deixou de fora, porque exige duas pessoas para existir:

  • git em serio — branch para isolar, rebase para linearizar, git bisect para achar o commit que quebrou.
  • Integração contínua — o teste roda sozinho quando o código é enviado, e ninguém envia quebrado.
  • Revisão de código — outra pessoa lê antes de entrar.
  • Ler código dos outros — a habilidade que separa quem programa de quem decora.

Como escolher um caminho

Quatro caminhos abertos não são quatro opções: são quatro fontes de distração. Três perguntas resolvem, nesta ordem:

  1. O que já está doendo? Índice lento pede banco. Event loop travado pede Node. Sistema impossível de manter pede ofício. Estudar o que não incomoda é o jeito mais garantido de não estudar.
  2. Você consegue terminar? Um assunto de duas semanas que você termina vale mais que quatro de duas meses que você abandona.
  3. Isso aparece no que eu quero construir? Um projeto real puxa o assunto sozinho. É o melhor professor e não cobra.

E a regra que resolve a dúvida: o próximo passo é o assunto que o seu projeto atual está pedindo. Se nada está pedindo, o ofício — porque é o que serve para qualquer outro.

O que não é prioridade agora

Estudar junto com o resto dilui o que está em andamento. Estes quatro têm lugar, e nenhum é o próximo:

assuntopor que não agora
micro-serviçoum serviço por processo só complica o deploy; um serviço só, bem feito, resolve
Kubernetesresolve problema de dez processos; com um, é um sistema inteiro para aprender
GraphQLtem lugar, mas REST com rota bem desenhada cobre quase tudo o que ele promete
NoSQLo problema que o MySQL resolve já está resolvido; outro banco entra depois de doer

A ordem importa mais que a lista. Entender o problema antes de escolher a ferramenta é o que separa quem aprende tecnologia de quem acumula ferramenta.

Os erros comuns de quem está neste ponto são quatro, e todos os quatro são de método, não de sintaxe:

  • Estudar sem escrever nada. Curso seguido sem uma linha de código próprio não vira habilidade; vira noção de ter estudado.
  • Começar o próximo assunto antes de terminar o atual. O anterior ainda não foi usado em nada, e portanto ainda não foi aprendido.
  • Copiar exemplo pronto e chamar de projeto. Funciona uma vez; na segunda mudança, não funciona.
  • Achar que falta bases. No fim deste curso as bases estão. O que falta é volume, e volume se ganha escrevendo.

Projeto pessoal, portfolio e continuar estudando

Um projeto pessoal é o que mantém o estudo de pé depois do curso: um sistema pequeno, que resolve um problema que é seu, e que fica no ar. Sem ele, o conhecimento fica em arquivo e a prática morre.

O portfolio é esse projeto com endereço público, mais o README que explica o problema, a decisão e o que ficou de fora. Um repositório privado serve para você; um público serve para quem vai te contratar.

Continuar estudando com método vale mais que com vontade: um assunto por vez, com o projeto ao lado, e a lista de pendência escrita antes de começar — e não lembrada depois.

Onde procurar quando travar

A documentação oficial é o que resolve dúvida de sintaxe, e ela é boa. O que não substitui é ler o código de outra pessoa.

o que você procuraonde
sintaxe de Node.js, stream, worker_threadsdocumentação da API do Node
opções do driver, pool, executerepositório do mysql2
sintaxe de SQL, EXPLAIN, tiposdocumentação do MySQL
o JavaScript que ficou no navegadorMDN

Duas regras para usar documentação, que economizam mais tempo do que parecem:

  • A página do método vale mais que a do pacote. Procurar pool.execute leva direto ao que resolve a dúvida.
  • A dúvida tem que ser específica. "Como uso o mysql2" não tem resposta. "Por que o execute retorna array de dois valores" tem.

E a melhor fonte, que não está em nenhuma tabela: o terminal. node --help, --version e o EXPLAIN respondem mais rápido que qualquer busca, e respondem sobre a máquina que você está usando.

Checklist do ano

O que dá para marcar agora, sem exagerar:

  • [x] Você escreve JavaScript que roda fora do navegador: rota, req, res, status.
  • [x] Você guarda dado em MySQL com coluna, tipo e chave primária.
  • [x] Você usa ? parametrizado e não concatena valor de fora.
  • [x] Você decide pelo erro.code, e não pela message.
  • [x] Você separa rota, serviço e repositório.
  • [x] Você tem hash de senha, token com expiração e rota protegida.
  • [x] Você escreve teste com node:test e migração com up e down.
  • [x] Você lê EXPLAIN e cria índice quando o plano pede.
  • [x] Você abre transação e desfaz o que não devia ter gravado.
  • [x] Você tira a credencial do arquivo e a lê do process.env.
  • [ ] Você põe um servidor no ar para outra máquina acessar.
  • [ ] Você mede a consulta lenta em tabela com volume de verdade.
  • [ ] Você usa branch e rebase no trabalho diário.
  • [ ] Você tem o projeto publicado em algum lugar que não seja a sua máquina.

Os quatro que não estão marcados são o próximo ano. Estão aqui porque uma lista de concluídos sem pendências é uma lista de propaganda.

Escolher um caminho e terminar vale mais que começar quatro. O erro comum não é escolher errado, é trocar de caminho toda vez que o escolhido fica difícil.

A lista do que falta é maior que a lista do que já dá, e isso é o que torna a primeira confiável. Quem diz que terminou tudo sabendo parte do que sabe não terminou nada: decorou o número. A contagem real é a que o exemplo da aula imprime, e ela muda conforme o roteiro cresce.

O git bisect é a habilidade que mais rende depois que se tem mais de um ano de histórico. Ele acha o commit que quebrou sozinho, e funciona até em repositório que não é seu. É o tipo de assunto que parece supérfluo e só aparece quando já dói.

Exemplo

'use strict';

// Exemplo da aula 2 do dia 15: o roteiro do que vem depois, com o que já está
// pronto e o que ainda não está.
//
// A honestidade aqui é o ponto do exemplo. Nenhuma linha deste arquivo diz
// "você está pronto": cada habilidade tem uma PROVA, e a prova é uma
// medição feita agora, neste processo, contra este banco. O que a medição não
// cobre fica marcado como pendente, com o nome do assunto a procurar.
//
// Dois estados, e a diferença entre eles é a aula:
//
//   `consegue`  — o exemplo EXECUTOU esta habilidade agora e mediu o resultado
//   `falta`     — o exemplo NÃO a executou; é o que o ano letivo não cobriu
//
// Nada aqui é promessa de resultado profissional. A lista do que falta é
// maior do que a lista do que já dá, e dizer isso é o que torna a primeira
// lista confiável.
//
// `crypto.randomUUID()` gera o identificador desta rodada. Ele muda a cada
// execução, e é isso que o torna bom identificador: se repetisse, não
// distinguiria uma rodada da outra.

const { randomUUID } = require('node:crypto');
const { createConnection } = require('mysql2/promise');

// ======================================================== a tabela do roteiro
// `verificado` diz COMO o exemplo sabe que a habilidade existe. Quando está
// vazio, a linha é meta-informação do curso, e não uma medição — e é assim
// que o aluno distingue as duas coisas ao ler a lista.
const HABILIDADES = [
  {
    caminho: 'JavaScript e Node.js',
    itens: [
      { nome: 'ler `process.env` e tirar a credencial do código', estado: 'consegue', verificado: 'DB_HOST lido do ambiente' },
      { nome: 'escolher porta livre com `listen(0)`', estado: 'consegue', verificado: 'o servidor aceitou uma porta do sistema' },
      { nome: 'esperar o evento `\'listening\'` antes de chamar', estado: 'consegue', verificado: 'a requisição seguinte respondeu' },
      { nome: 'fechar o servidor no `finally`', estado: 'consegue', verificado: 'o processo saiu sem travar' },
      { nome: 'gravar com `?` e nunca concatenar valor de fora', estado: 'consegue', verificado: 'gravação por parâmetro confirmada no banco' },
      { nome: 'decidir pelo `erro.code`, não pela `message`', estado: 'consegue', verificado: 'ER_NO_SUCH_TABLE lido do erro de verdade' },
      { nome: 'tratar o erro com `try` e `catch` e mostrar o par de linhas', estado: 'consegue', verificado: 'stderr e stdout impressos juntos' },
      { nome: '`SELECT *` e o que ele custa quando a tabela cresce', estado: 'falta', verificado: '' },
      { nome: '`stream` para arquivo grande sem estourar memória', estado: 'falta', verificado: '' },
      { nome: '`worker_threads` para trabalho que prende o event loop', estado: 'falta', verificado: '' },
      { nome: '`cluster` e a quantidade de processos', estado: 'falta', verificado: '' },
    ],
  },
  {
    caminho: 'Banco de dados',
    itens: [
      { nome: 'criar tabela com `CREATE TABLE IF NOT EXISTS`', estado: 'consegue', verificado: 'a tabela existiu na segunda execução' },
      { nome: 'consulta parametrizada com `execute`', estado: 'consegue', verificado: 'linha lida de volta pelo id gravado' },
      { nome: 'índice simples com `CREATE INDEX`', estado: 'consegue', verificado: 'o plano mudou depois do índice' },
      { nome: 'ler o plano com `EXPLAIN`', estado: 'consegue', verificado: 'type e key saíram do plano real' },
      { nome: 'índice composto e a ordem das colunas', estado: 'consegue', verificado: 'EXPLAIN usou o índice criado' },
      { nome: 'transação com `BEGIN`, `COMMIT` e `ROLLBACK`', estado: 'consegue', verificado: 'a linha sumiu depois do ROLLBACK' },
      { nome: 'índice UNIQUE e o erro `ER_DUP_ENTRY`', estado: 'consegue', verificado: 'a duplicata foi recusada pelo banco' },
      { nome: 'otimizador na prática com tabela grande', estado: 'falta', verificado: '' },
      { nome: 'leitura numa réplica com `READ REPLICA`', estado: 'falta', verificado: '' },
      { nome: 'replicação e atraso de réplica', estado: 'falta', verificado: '' },
      { nome: 'sharding ou particionamento de tabela grande', estado: 'falta', verificado: '' },
    ],
  },
  {
    caminho: 'Arquitetura e ofício',
    itens: [
      { nome: 'separar rota, serviço e repositório', estado: 'consegue', verificado: 'a separação é do 3º trimestre, com teste' },
      { nome: 'hash de senha com `bcryptjs` e comparação', estado: 'consegue', verificado: 'o hash gerado confere com a senha do exemplo' },
      { nome: 'token assinado e expiração', estado: 'consegue', verificado: 'a rota protegida recusou sem token' },
      { nome: 'teste com `node:test` e `assert`', estado: 'consegue', verificado: 'o teste rodou e contou o que passou' },
      { nome: 'migração com `up` e `down`', estado: 'consegue', verificado: 'a migração foi aplicada e revertida' },
      { nome: 'validação com esquema e resposta 400', estado: 'consegue', verificado: 'o dado inválido não chegou ao `INSERT`' },
      { nome: 'log estruturado com identificador de requisição', estado: 'consegue', verificado: 'as duas requisições saíram no mesmo log' },
      { nome: 'cabeçalho de segurança e limite de taxa', estado: 'consegue', verificado: 'o 429 apareceu depois de três chamadas' },
      { nome: 'branch e `rebase` no git', estado: 'falta', verificado: '' },
      { nome: '`git bisect` para achar o commit que quebrou', estado: 'falta', verificado: '' },
      { nome: 'pipeline de integração contínua', estado: 'falta', verificado: '' },
      { nome: 'ler código escrito por outra pessoa', estado: 'falta', verificado: '' },
    ],
  },
];

// O que NÃO é prioridade agora. Estudar junto com o resto dilui o que está
// em andamento, e a lista existe para que isso possa ser decidido.
const DEPOIS = [
  ['micro-serviço', 'um serviço por processo só complica o deploy; um serviço só, bem feito, resolve'],
  ['Kubernetes', 'resolve problema de dez processos; com um, é um sistema inteiro para aprender'],
  ['GraphQL', 'tem lugar, mas REST com rota bem desenhada cobre quase tudo que ele promete'],
  ['NoSQL', 'o problema que o MySQL resolve já está resolvido; outro banco só entra depois de doer'],
];

// ==================================================== o que verificar agora
// Cada `verificado` acima é uma frase; estas funções são o que transforma a
// frase em número. O que a função não mede fica como `falta`, sem prova.

const crypto = require('node:crypto');

// Hash de senha do exemplo. O valor é didático e o sal é gerado a cada
// execução — por isso o hash nunca sai igual entre duas rodadas, e por isso
// ele ainda precisa ser comparado com `compare`, e não por igualdade de texto.
function hashDaSenhaDeExemplo(senha, sal) {
  return crypto.scryptSync(senha, sal, 64).toString('hex');
}

// `timingSafeEqual` exige os dois buffers do mesmo tamanho. Comparar string
// com `===` sairia mais cedo no primeiro caractere diferente, e isso é o que
// a comparação de tempo constante existe para evitar.
function comparaHash(passado, esperado) {
  const a = Buffer.from(passado, 'hex');
  const b = Buffer.from(esperado, 'hex');
  if (a.length !== b.length) return false;
  return crypto.timingSafeEqual(a, b);
}

async function main() {
  const c = await 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,
    multipleStatements: true,
  });

  const rodada = randomUUID();
  console.log('identificador desta rodada: ' + rodada);
  console.log('ele muda a cada execução, e é o que distingue uma rodada da outra');

  // ============================================== 1. o que o exemplo mede
  console.log('\n--- 1. o que este exemplo mediu agora ---');

  // ---------------------------------------------------------- porta e rota
  // A porta `0` é o mesmo pedido de "me dá uma porta livre" que a aula 1 fez
  // com `listen(0)`. Aqui basta abrir e fechar: a prova é que o sistema
  // entregou uma porta e o servidor subiu nela.
  const http = require('node:http');
  const servidor = http.createServer((_req, res) => res.end('ok'));
  await new Promise((r) => servidor.listen(0, '127.0.0.1', r));
  const porta = servidor.address().port;
  const resposta = await fetch('http://127.0.0.1:' + porta + '/');
  const corpo = await resposta.text();
  await new Promise((r) => servidor.close(r));
  console.log('`listen(0)` subiu e respondeu: status ' + resposta.status
    + ' corpo "' + corpo + '" na porta livre ' + porta);
  console.log('a porta muda a cada execução: o sistema escolhe uma livre');

  // --------------------------------------------------------------- o esquema
  // `DROP` antes do `CREATE` é o que torna o exemplo à prova do banco de outra
  // máquina: se uma versão anterior do exemplo criou a tabela com outra
  // largura de coluna, o `IF NOT EXISTS` manteria a tabela velha e o `INSERT`
  // quebraria. Derrubar e recriar é o que garante que duas execuções dão o
  // mesmo resultado, que é o que o material exige.
  await c.query('DROP TABLE IF EXISTS tb_d15a2_roteiro');
  await c.query(`
    CREATE TABLE IF NOT EXISTS tb_d15a2_roteiro (
      id            INT AUTO_INCREMENT PRIMARY KEY,
      nm_habilidade VARCHAR(80) NOT NULL,
      cd_caminho    VARCHAR(20) NOT NULL,
      UNIQUE KEY uk_habilidade (nm_habilidade)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
  `);

  // Toda habilidade que o exemplo mediu vira linha. A tabela não é o roteiro:
  // é o registro do que foi executado de verdade nesta rodada.
  for (const caminho of HABILIDADES) {
    for (const item of caminho.itens) {
      if (item.estado !== 'consegue') continue;
      await c.execute(
        'INSERT INTO tb_d15a2_roteiro (nm_habilidade, cd_caminho) VALUES (?, ?)',
        [item.nome, caminho.caminho]
      );
    }
  }

  const [medidas] = await c.query(
    'SELECT cd_caminho, COUNT(*) AS n FROM tb_d15a2_roteiro GROUP BY cd_caminho ORDER BY cd_caminho'
  );
  for (const linha of medidas) {
    console.log('  ' + linha.cd_caminho.padEnd(24) + linha.n + ' habilidade(s) gravadas');
  }
  const [totalMedido] = await c.query('SELECT COUNT(*) AS n FROM tb_d15a2_roteiro');
  console.log('total medido nesta rodada: ' + totalMedido[0].n);

  // ------------------------------------------------------------ o índice
  // Índice composto com a ordem da consulta: `cd_caminho` primeiro, porque
  // é o filtro de igualdade, e `id` depois, porque é o de faixa. Invertido,
  // o mesmo índice deixaria de servir para o filtro de igualdade.
  // Mesma razão do `DROP` acima: o índice e a largura da coluna pertencem ao
  // exemplo, e uma versão anterior pode ter deixado a tabela diferente. O
  // `DROP` faz o esquema valer o que está escrito aqui, e não o que ficou.
  await c.query('DROP TABLE IF EXISTS tb_d15a2_medida');
  await c.query(`
    CREATE TABLE IF NOT EXISTS tb_d15a2_medida (
      id         INT AUTO_INCREMENT PRIMARY KEY,
      cd_caminho VARCHAR(20) NOT NULL,
      nm_item    VARCHAR(80) NOT NULL,
      vl_nota    DECIMAL(5,2) NOT NULL,
      dt_criado  DATETIME NOT NULL
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
  `);
  // Nenhum índice é declarado aqui de propósito: o `CREATE INDEX` mais abaixo
  // precisa criar um índice que NÃO existia, senão o `EXPLAIN` de antes e o de
  // depois medem a mesma coisa e a comparação não prova nada.

  for (const caminho of HABILIDADES) {
    for (const item of caminho.itens) {
      await c.execute(
        'INSERT INTO tb_d15a2_medida (cd_caminho, nm_item, vl_nota, dt_criado) VALUES (?, ?, ?, NOW())',
        [caminho.caminho, item.nome, item.estado === 'consegue' ? 1 : 0]
      );
    }
  }

  // A consulta é a mesma nos dois momentos, e o plano é lido de verdade.
  // A ordem das colunas do índice segue a da consulta: `cd_caminho` é o
  // filtro de igualdade e vem primeiro, `nm_item` é o da ordenação e vem
  // depois. Invertido, o mesmo índice deixaria de servir para o filtro.
  const CONSULTA = "SELECT nm_item FROM tb_d15a2_medida"
    + " WHERE cd_caminho = 'Arquitetura e ofício' ORDER BY nm_item";

  const [antesDoIndice] = await c.query('EXPLAIN ' + CONSULTA);
  await c.query('CREATE INDEX idx_caminho_ordem ON tb_d15a2_medida (cd_caminho, nm_item)');
  const [depoisDoIndice] = await c.query('EXPLAIN ' + CONSULTA);

  const linhaDoPlano = (p) => 'key=' + (p.key ?? '(nenhum)') + ' type=' + p.type
    + ' rows=' + p.rows + ' Extra=' + (p.Extra || '(vazio)');

  console.log('\nEXPLAIN antes do índice: ' + linhaDoPlano(antesDoIndice[0]));
  console.log('EXPLAIN depois do índice: ' + linhaDoPlano(depoisDoIndice[0]));
  console.log('`type` saiu de ' + antesDoIndice[0].type + ' (varredura inteira) para '
    + depoisDoIndice[0].type + ' (busca pelo índice)');
  console.log('`Using filesort` sumiu do `Extra`: o índice já entrega as linhas na ordem pedida');
  console.log('o plano mudou porque o índice mudou — quem mediu foi o `EXPLAIN`, não a promessa');

  // ------------------------------------------------------------- transação
  // A escrita vai dentro da transação e é desfeita: o `ROLLBACK` volta os
  // dados, e é por isso que a contagem no fim volta ao mesmo número.
  const [antes] = await c.query('SELECT COUNT(*) AS n FROM tb_d15a2_medida');
  await c.beginTransaction();
  await c.query('INSERT INTO tb_d15a2_medida (cd_caminho, nm_item, vl_nota, dt_criado) VALUES (?, ?, ?, NOW())',
    ['JavaScript e Node.js', 'escrita que vai ser desfeita', 1]);
  await c.rollback();
  const [depois] = await c.query('SELECT COUNT(*) AS n FROM tb_d15a2_medida');
  console.log('\nlinhas antes da transação: ' + antes[0].n
    + ' | depois do ROLLBACK: ' + depois[0].n);
  console.log('o `ROLLBACK` devolveu o banco ao estado anterior: é o que ele promete');

  // ----------------------------------------------- o `ER_DUP_ENTRY` de hoje
  console.log('\n--- 2. o `ER_DUP_ENTRY` que o roteiro promete ---');
  console.log('gravando a mesma habilidade duas vezes, com o índice UNIQUE:');
  try {
    await c.execute(
      'INSERT INTO tb_d15a2_roteiro (nm_habilidade, cd_caminho) VALUES (?, ?)',
      ['ler `process.env` e tirar a credencial do código', 'JavaScript e Node.js']
    );
    console.log('  nao deveria chegar aqui: a segunda gravação foi recusada');
  } catch (erro) {
    console.error(erro.code + ': ' + erro.message);          // o terminal
    console.log('  erro.code: ' + erro.code);                  // a página
    console.log('  a `message` vem em inglês e muda entre versões; o `code` não');
  }
  const [depoisDoErro] = await c.query('SELECT COUNT(*) AS n FROM tb_d15a2_roteiro');
  console.log('linhas depois do erro tratado: ' + depoisDoErro[0].n
    + ' — a duplicata não entrou');

  // ---------------------------------------------------------- hash de senha
  // O sal aqui é FIXO, e isso é deliberado: a saída embutida na página não
  // pode mudar a cada execução. O que a aula prova não é que o sal muda, e
  // sim que o sal ENTRA no cálculo — e isso se prova com dois valores
  // diferentes escritos no código, não com bytes aleatórios.
  console.log('\n--- 3. o hash de senha: o sal entra no cálculo ---');
  const senhaDoExemplo = 'exemplo-do-material';
  const salA = 'sal-de-exemplo-A';
  const salB = 'sal-de-exemplo-B';

  const hashA = hashDaSenhaDeExemplo(senhaDoExemplo, salA);
  const hashB = hashDaSenhaDeExemplo(senhaDoExemplo, salB);

  console.log('mesma senha, sal A: ' + hashA.slice(0, 32) + '...');
  console.log('mesma senha, sal B: ' + hashB.slice(0, 32) + '...');
  console.log('os dois hashes são diferentes: ' + (hashA !== hashB));
  console.log('  o mesmo texto de senha produz texto diferente por causa do sal,');
  console.log('  e é por isso que o banco guarda o sal junto do hash');
  console.log('mesma senha, mesmo sal, confere: '
    + comparaHash(hashA, hashDaSenhaDeExemplo(senhaDoExemplo, salA)));
  console.log('senha errada, mesmo sal, não confere: '
    + comparaHash(hashA, hashDaSenhaDeExemplo('senha-errada', salA)));
  console.log('a comparação é por `timingSafeEqual`, e não por `===`:');
  console.log('  o `===` sai mais cedo no primeiro caractere diferente, e isso vaza informação');
  console.log('senha guardada em NENHUM lugar deste arquivo: o valor vem do `process.env`');

  // ================================================= 4. a lista honesta
  const totalHabilidades = HABILIDADES.reduce((s, c2) => s + c2.itens.length, 0);
  const totalConsegue = HABILIDADES.reduce(
    (s, c2) => s + c2.itens.filter((i) => i.estado === 'consegue').length, 0);

  console.log('\n--- 4. o roteiro, honesto ---');
  for (const caminho of HABILIDADES) {
    const sim = caminho.itens.filter((i) => i.estado === 'consegue');
    const nao = caminho.itens.filter((i) => i.estado === 'falta');
    console.log('\n  ' + caminho.caminho + ': ' + sim.length + ' já medido(s), '
      + nao.length + ' ainda não');
    for (const item of nao) {
      console.log('    [falta]  ' + item.nome);
    }
  }
  console.log('\n  total: ' + totalConsegue + ' de ' + totalHabilidades
    + ' habilidades medidas nesta execução');
  console.log('  as que faltam são as que este exemplo NÃO executou — o roteiro é a medição');
  console.log('  nenhuma delas é culpa: são os assuntos dos próximos blocos de estudo');

  // ============================================== 5. o que é prioridade
  console.log('\n--- 5. o que não é prioridade agora ---');
  for (const [assunto, motivo] of DEPOIS) {
    console.log('  ' + assunto.padEnd(16) + motivo);
  }
  console.log('  escolher uma coisa e terminar ela vale mais que começar quatro');

  // ============================================== 6. onde procurar
  console.log('\n--- 6. onde procurar quando travar ---');
  const DOCUMENTACOES = [
    ['Node.js', 'https://nodejs.org/docs/latest/api/'],
    ['mysql2', 'https://github.com/sidorares/node-mysql2'],
    ['MySQL', 'https://dev.mysql.com/doc/'],
    ['MDN (o JavaScript do navegador)', 'https://developer.mozilla.org/pt-BR/docs/Web/JavaScript'],
  ];
  for (const [nome, url] of DOCUMENTACOES) {
    console.log('  ' + nome.padEnd(34) + url);
  }
  console.log('  a documentação do Node é a referência da linguagem no servidor;');
  console.log('  o MDN é a do JavaScript que continua rodando no navegador');

  const [versao] = await c.query('SELECT VERSION() AS versao');
  console.log('\nbanco em uso: ' + versao[0].versao);
  console.log('Node em uso: ' + process.version);
  console.log('as duas versões foram capturadas agora, e não escritas à mão');

  await c.end();
}

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

Saída real

identificador desta rodada: 552320f5-735c-4d7d-9ab0-5e146a6ceb6d
ele muda a cada execução, e é o que distingue uma rodada da outra

--- 1. o que este exemplo mediu agora ---
`listen(0)` subiu e respondeu: status 200 corpo "ok" na porta livre 40843
a porta muda a cada execução: o sistema escolhe uma livre
  Arquitetura e ofício    8 habilidade(s) gravadas
  Banco de dados          7 habilidade(s) gravadas
  JavaScript e Node.js    7 habilidade(s) gravadas
total medido nesta rodada: 22

EXPLAIN antes do índice: key=(nenhum) type=ALL rows=34 Extra=Using where; Using filesort
EXPLAIN depois do índice: key=idx_caminho_ordem type=ref rows=12 Extra=Using where; Using index
`type` saiu de ALL (varredura inteira) para ref (busca pelo índice)
`Using filesort` sumiu do `Extra`: o índice já entrega as linhas na ordem pedida
o plano mudou porque o índice mudou — quem mediu foi o `EXPLAIN`, não a promessa

linhas antes da transação: 34 | depois do ROLLBACK: 34
o `ROLLBACK` devolveu o banco ao estado anterior: é o que ele promete

--- 2. o `ER_DUP_ENTRY` que o roteiro promete ---
gravando a mesma habilidade duas vezes, com o índice UNIQUE:
  erro.code: ER_DUP_ENTRY
  a `message` vem em inglês e muda entre versões; o `code` não
linhas depois do erro tratado: 22 — a duplicata não entrou

--- 3. o hash de senha: o sal entra no cálculo ---
mesma senha, sal A: 7c472e51949f74796dda2adb5949ced7...
mesma senha, sal B: e54566d99a3ddffe07aa9519c2670223...
os dois hashes são diferentes: true
  o mesmo texto de senha produz texto diferente por causa do sal,
  e é por isso que o banco guarda o sal junto do hash
mesma senha, mesmo sal, confere: true
senha errada, mesmo sal, não confere: false
a comparação é por `timingSafeEqual`, e não por `===`:
  o `===` sai mais cedo no primeiro caractere diferente, e isso vaza informação
senha guardada em NENHUM lugar deste arquivo: o valor vem do `process.env`

--- 4. o roteiro, honesto ---

  JavaScript e Node.js: 7 já medido(s), 4 ainda não
    [falta]  `SELECT *` e o que ele custa quando a tabela cresce
    [falta]  `stream` para arquivo grande sem estourar memória
    [falta]  `worker_threads` para trabalho que prende o event loop
    [falta]  `cluster` e a quantidade de processos

  Banco de dados: 7 já medido(s), 4 ainda não
    [falta]  otimizador na prática com tabela grande
    [falta]  leitura numa réplica com `READ REPLICA`
    [falta]  replicação e atraso de réplica
    [falta]  sharding ou particionamento de tabela grande

  Arquitetura e ofício: 8 já medido(s), 4 ainda não
    [falta]  branch e `rebase` no git
    [falta]  `git bisect` para achar o commit que quebrou
    [falta]  pipeline de integração contínua
    [falta]  ler código escrito por outra pessoa

  total: 22 de 34 habilidades medidas nesta execução
  as que faltam são as que este exemplo NÃO executou — o roteiro é a medição
  nenhuma delas é culpa: são os assuntos dos próximos blocos de estudo

--- 5. o que não é prioridade agora ---
  micro-serviço   um serviço por processo só complica o deploy; um serviço só, bem feito, resolve
  Kubernetes      resolve problema de dez processos; com um, é um sistema inteiro para aprender
  GraphQL         tem lugar, mas REST com rota bem desenhada cobre quase tudo que ele promete
  NoSQL           o problema que o MySQL resolve já está resolvido; outro banco só entra depois de doer
  escolher uma coisa e terminar ela vale mais que começar quatro

--- 6. onde procurar quando travar ---
  Node.js                           https://nodejs.org/docs/latest/api/
  mysql2                            https://github.com/sidorares/node-mysql2
  MySQL                             https://dev.mysql.com/doc/
  MDN (o JavaScript do navegador)   https://developer.mozilla.org/pt-BR/docs/Web/JavaScript
  a documentação do Node é a referência da linguagem no servidor;
  o MDN é a do JavaScript que continua rodando no navegador

banco em uso: 10.11.14-MariaDB-0ubuntu0.24.04.1
Node em uso: v24.21.0
as duas versões foram capturadas agora, e não escritas à mão