Dia 15 — Fechamento do ano letivo
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 navegador | no servidor | |
|---|---|---|
| o que o código é | um arquivo carregado pela página | um processo que fica rodando |
| o objeto global | document | process |
| quem chama o código | a pessoa, clicando | a rede, mandando requisição |
| como o código recebe o pedido | o target do evento | o req, objeto da requisição |
| como o código responde | escrevendo no DOM | res.status e res.end() |
| onde o dado fica | localStorage, texto | tabela no MySQL, com coluna e tipo |
| o que acontece quando acaba | a aba fecha e o dado some | o processo morre e o dado fica |
| quem dá o tempo | o evento do usuário | o 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 navegador | no servidor | o que a troca exige |
|---|---|---|
document.querySelector | req.url | a 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 keydown | servidor.on('request', fn) | o que chega é requisição, com método e com corpo |
localStorage.setItem | INSERT INTO ... VALUES (?, ?) | o dado ganha coluna, tipo e chave primária |
localStorage.getItem | SELECT ... 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.stringify | os mesmos dois | JSON é 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. asynceawait. 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.tryecatch. O bloco é idêntico. Mudou o que se compara dentro dele:erro.codediz o que aconteceu, eerro.messagedo 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,finallye o encadeamento comawaitsã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.
| bloco | onde o código rodava | o que era |
|---|---|---|
| 1º trimestre | dentro da aba | sintaxe, função, async/await, evento, DOM |
| 2º trimestre | num processo | require, rota HTTP, req e res, MySQL, consulta parametrizada |
| 3º trimestre | num processo que precisa sobreviver | camadas, 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.
| comando | o que resolve |
|---|---|
node --check arquivo.js | dizer se o arquivo tem erro de sintaxe, sem executar nada |
node -r ./_com_mysql.js exemplo.js | carregar o harness, que lê o .env e abre a conexão |
.env com process.env | tirar a credencial do arquivo de código e tirá-la do git |
node --test | rodar 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 ROLLBACK | agrupar as escritas, ou desfazer o que não devia ter gravado |
pm2 start servidor.js | fazer o processo voltar sozinho quando ele cai |
docker compose up | subir aplicação e banco juntos, em qualquer máquina |
git log --oneline | guardar 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.enve 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 comclose()nofinally. - Você monta uma rota que decide o
statuse 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 amessage. - 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
TRUNCATEestá no começo.
O
TRUNCATEno começo e não no fim é detalhe pequeno que evita defeito grande: comDELETEoAUTO_INCREMENTcontinua 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
fetchpara a própria API. A igualdade sai deJSON.stringifysobre 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
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 doevent looppara 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.
EXPLAINde 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 —
RabbitMQouAmazon 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 —
Expresscobre o que os exemplos deste curso fizeram na mão. Serve quando a rota começa a repetir validação,try/catche 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:
gitem serio —branchpara isolar,rebasepara linearizar,git bisectpara 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:
- 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.
- Você consegue terminar? Um assunto de duas semanas que você termina vale mais que quatro de duas meses que você abandona.
- 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:
| assunto | por que não 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 o que ele promete |
| NoSQL | o 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ê procura | onde |
|---|---|
sintaxe de Node.js, stream, worker_threads | documentação da API do Node |
opções do driver, pool, execute | repositório do mysql2 |
sintaxe de SQL, EXPLAIN, tipos | documentação do MySQL |
| o JavaScript que ficou no navegador | MDN |
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.executeleva 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
executeretorna 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 pelamessage. - [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:teste migração comupedown. - [x] Você lê
EXPLAINe 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
brancherebaseno 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