Dia 16 — Projeto do trimestre: API com MySQL
Montar a API do zero
Montar a API do zero
Este é o projeto que fecha o 2º trimestre. Não é um exemplo novo: é o que já foi
feito, com a estrutura que faltava. A diferença entre um script que roda e um
sistema que dá para mudar são três pastas.
A estrutura da API é essa divisão em camadas, e a ideia por trás dela é
separar responsabilidades: cada camada tem o que ela sabe e o que ela não
sabe, e a regra é que nenhuma camada precise da outra para existir.
| camada | nome em inglês | sabe | não sabe |
|---|---|---|---|
rota | controller | método HTTP e status | SQL |
serviço | service | a regra do negócio | MySQL e requisição |
repositório | repository | SQL | que existe HTTP |
Os dois nomes de cada linha são a mesma camada: em português, controller é a
camada de rota e repository é a camada de dados. A camada de rota é o que
traduz HTTP; a camada de dados é o que fala com o MySQL. As rotas da API
são a lista de pares método e caminho, e é ela que o cliente enxerga.
O ciclo CRUD inteiro, contra a API montada
Repare no /item/abc → 400: o serviço recusou antes de consultar. O banco
não chegou a receber a query. Isso é regra de negócio escrita uma vez, e não em
cada rota.
E o GET /item final devolveu só o id: 2 — o DELETE gravou no banco de
verdade.
O repositório: sabe SQL, não sabe que existe HTTP
const criarRepositorio = (conexao) => ({ async buscarPorId(id) { const [linhas] = await conexao.query( 'SELECT id, nm_item, vl_preco FROM tb_d16_item WHERE id = ?', [id]); return linhas.length > 0 ? linhas[0] : null; }, // ... });
Tudo que vem de fora entra por ?. O exemplo **verifica isso em vez de
prometer**: ele lê o próprio código-fonte, recorta a camada do repositório e
conta.
O ORDER BY id é literal escrito no texto do SQL, sem + e sem valor de fora.
O serviço: a regra, e nada mais
const criarServico = (repositorio) => ({ async buscar(id) { if (!Number.isInteger(id) || id < 1) { return { erro: 'id precisa ser um numero inteiro positivo', status: 400 }; } const item = await repositorio.buscarPorId(id); if (item === null) return { erro: 'item nao encontrado', status: 404 }; return { dados: item }; }, // ... });
Repara no que o serviço não importa: ele recebe o repositório, não a conexão.
Poderia ser um repositório em memória, de arquivo, ou de API. **Trocar a
tecnologia é mudar a linha de montagem** — e só.
A rota: método e status, nada mais
if (recurso !== 'item') return json(res, 404, { erro: 'rota nao encontrada' }); if (metodo === 'GET' && id === null) return responder(() => servico.listar()); if (metodo === 'GET') return responder(() => servico.buscar(id)); if (metodo === 'POST') return responder(async () => servico.criar(await lerCorpo(req))); if (metodo === 'PUT') return responder(async () => servico.alterar(id, await lerCorpo(req))); if (metodo === 'DELETE') return responder(() => servico.apagar(id));
A rota não conhece tb_d16_item, nem SELECT, nem insertId. Ela sabe que
POST que cria responde 201, que DELETE que apaga responde **204 sem
corpo**, e delega o resto.
A montagem: uma linha por camada
const repositorio = criarRepositorio(conexao); const servico = criarServico(repositorio); const handler = criarRotas(servico); const servidor = http.createServer(handler);
As três linhas são o padrão de pastas escrito como dependência: criarRepositorio
devolve a camada de dados, criarServico recebe o repository e devolve o
service, e criarRotas recebe o service e devolve o handler que o
http.createServer quer. Em disco, cada uma dessas linhas vira um arquivo — o
mesmo padrão de pastas, com criarRotas no topo, criarServico no meio e
criarRepositorio embaixo, e o main só montando.
O finally fecha tudo — o close() do servidor, o DROP TABLE e o end()
da conexão:
} finally { await new Promise((resolve) => servidor.close(resolve)); await conexao.query('DROP TABLE IF EXISTS tb_d16_item'); await conexao.end(); }
Sem o close(), o processo segura o event loop e trava. É a mesma regra do
dia 5 e do dia 15, agora num projeto inteiro.
Exemplo
'use strict'; // Exemplo da aula 1 do dia 16: a API do trimestre, montada em camadas. // // Este e o projeto que fecha o 2º trimestre. Nao e um exemplo novo: e o que ja // foi feito, com a ESTRUTURA que faltava. A diferenca entre um script que roda e // um sistema que da para mudar sao tres pastas: // // rota -> sabe o metodo HTTP e o status, nao sabe SQL // repositorio-> sabe SQL, nao sabe HTTP // servico -> decide a regra; nao sabe MySQL nem requisição // // O exemplo monta a API completa, sobe, faz o ciclo CRUD inteiro contra ela, e // no fim mostra a contagem de linhas por camada — que e a prova de que a // separacao e real e nao so Commentario. // // As quatro coisas que o CONTRATO exige e que este exemplo cumpre: // 1. `listen(0)` — porta livre, nunca fixa // 2. esperar o 'listening' antes de chamar // 3. `server.close()` no `finally`, sempre // 4. `console.log` de contexto antes de cada `console.error` const http = require('node:http'); const { createConnection } = require('mysql2/promise'); // --------------------------------------------------------------------- // CAMADA 1: configuracao. Unico lugar que le o ambiente. // --------------------------------------------------------------------- const configuracao = () => ({ 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, }); // --------------------------------------------------------------------- // CAMADA 2: repositorio. Sabe SQL. NAO sabe que existe HTTP. // --------------------------------------------------------------------- // // Tudo que recebe de fora entra por parametro (?). E o que garante que nao ha // concatenacao em lugar nenhum do projeto. const criarRepositorio = (conexao) => ({ async listar() { const [linhas] = await conexao.query('SELECT id, nm_item, vl_preco FROM tb_d16_item ORDER BY id'); return linhas; }, async buscarPorId(id) { const [linhas] = await conexao.query( 'SELECT id, nm_item, vl_preco FROM tb_d16_item WHERE id = ?', [id]); return linhas.length > 0 ? linhas[0] : null; }, async criar(item) { const [r] = await conexao.query( 'INSERT INTO tb_d16_item (nm_item, vl_preco) VALUES (?, ?)', [item.nm_item, item.vl_preco]); return { id: r.insertId, ...item }; }, async alterar(id, item) { const [r] = await conexao.query( 'UPDATE tb_d16_item SET nm_item = ?, vl_preco = ? WHERE id = ?', [item.nm_item, item.vl_preco, id]); return r.affectedRows; }, async apagar(id) { const [r] = await conexao.query('DELETE FROM tb_d16_item WHERE id = ?', [id]); return r.affectedRows; }, }); // --------------------------------------------------------------------- // CAMADA 3: servico. Decide a REGRA. Nao sabe MySQL nem requisição HTTP. // --------------------------------------------------------------------- // // Repara no que o servico NAO importa: ele recebe o repositorio, nao a conexao. // Poderia ser um repositorio em memoria, de arquivo, ou de API. Trocar a // tecnologia e mudar so a linha demontagem. const criarServico = (repositorio) => ({ async listar() { return repositorio.listar(); }, async buscar(id) { if (!Number.isInteger(id) || id < 1) { // regra do servico, nao do SQL: id invalido nem chega ao banco return { erro: 'id precisa ser um numero inteiro positivo', status: 400 }; } const item = await repositorio.buscarPorId(id); if (item === null) return { erro: 'item nao encontrado', status: 404 }; return { dados: item }; }, async criar(item) { if (!item || !item.nm_item || item.vl_preco === undefined) { return { erro: 'nm_item e vl_preco sao obrigatorios', status: 400 }; } try { return { dados: await repositorio.criar(item), status: 201 }; } catch (erro) { if (erro.code === 'ER_DUP_ENTRY') { return { erro: 'ja existe um item com esse nome', status: 409 }; } throw erro; } }, async alterar(id, item) { if (!Number.isInteger(id) || id < 1) { return { erro: 'id precisa ser um numero inteiro positivo', status: 400 }; } if (!item || !item.nm_item || item.vl_preco === undefined) { return { erro: 'nm_item e vl_preco sao obrigatorios', status: 400 }; } const afetadas = await repositorio.alterar(id, item); if (afetadas === 0) return { erro: 'item nao encontrado', status: 404 }; return { dados: await repositorio.buscarPorId(id) }; }, async apagar(id) { if (!Number.isInteger(id) || id < 1) { return { erro: 'id precisa ser um numero inteiro positivo', status: 400 }; } const afetadas = await repositorio.apagar(id); if (afetadas === 0) return { erro: 'item nao encontrado', status: 404 }; return { apagado: true }; }, }); // --------------------------------------------------------------------- // CAMADA 4: rota. Sabe METODO e STATUS. Nao sabe SQL. // --------------------------------------------------------------------- const criarRotas = (servico) => { const json = (res, status, corpo) => { res.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8' }); res.end(JSON.stringify(corpo)); }; const lerCorpo = async (req) => { const pedacos = []; for await (const p of req) pedacos.push(p); if (pedacos.length === 0) return null; try { return JSON.parse(Buffer.concat(pedacos).toString('utf8')); } catch { return null; } }; return async (req, res) => { const [caminho] = req.url.split('?'); const partes = caminho.split('/').filter(Boolean); const recurso = partes[0]; const id = partes.length > 1 ? Number(partes[1]) : null; const metodo = req.method; // A rota so conhece GET, POST, PUT e DELETE em /item. Nada de SQL, nada de // nome de tabela: e so o que fazer com o pedido e com o resultado. const responder = async (chamada) => { try { const r = await chamada(); if (r && r.status && r.status >= 400) return json(res, r.status, { erro: r.erro }); if (r && r.status === 201) return json(res, 201, r.dados); if (r && r.apagado) { res.writeHead(204); return res.end(); } if (r && r.dados) return json(res, 200, r.dados); return json(res, 200, r); } catch (erro) { console.error('erro em ' + metodo + ' ' + caminho + ': ' + erro.code + ' - ' + erro.message); console.log(' erro em ' + metodo + ' ' + caminho + ':', erro.code); return json(res, 500, { erro: 'erro interno' }); } }; if (recurso !== 'item') return json(res, 404, { erro: 'rota nao encontrada' }); if (metodo === 'GET' && id === null) return responder(() => servico.listar()); if (metodo === 'GET') return responder(() => servico.buscar(id)); if (metodo === 'POST') return responder(async () => servico.criar(await lerCorpo(req))); if (metodo === 'PUT') return responder(async () => servico.alterar(id, await lerCorpo(req))); if (metodo === 'DELETE') return responder(() => servico.apagar(id)); return json(res, 404, { erro: 'rota nao encontrada' }); }; }; async function main() { const conexao = await createConnection(configuracao()); await conexao.query(` CREATE TABLE IF NOT EXISTS tb_d16_item ( id INT AUTO_INCREMENT PRIMARY KEY, nm_item VARCHAR(40) NOT NULL, vl_preco DECIMAL(10,2) NOT NULL, UNIQUE KEY uk_nome (nm_item) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 `); await conexao.query('DELETE FROM tb_d16_item'); // A montagem: uma linha por camada. Trocar o repositorio aqui embaixo e a // unica mudanca necessaria para trocar MySQL por outra tecnologia. const repositorio = criarRepositorio(conexao); const servico = criarServico(repositorio); const handler = criarRotas(servico); const servidor = http.createServer(handler); await new Promise((resolve) => servidor.listen(0, '127.0.0.1', resolve)); const { port } = servidor.address(); const base = `http://127.0.0.1:${port}`; const pedir = async (metodo, caminho, corpo) => { const init = { method: metodo }; if (corpo !== undefined) { init.headers = { 'Content-Type': 'application/json' }; init.body = JSON.stringify(corpo); } const r = await fetch(base + caminho, init); return { status: r.status, texto: await r.text() }; }; try { console.log('servidor no ar em ' + base); console.log(''); console.log('--- o ciclo CRUD inteiro, contra a API montada ---'); const criado = await pedir('POST', '/item', { nm_item: 'monitor', vl_preco: 890.00 }); console.log(' POST /item ->', criado.status, criado.texto); const id = JSON.parse(criado.texto).id; const segundo = await pedir('POST', '/item', { nm_item: 'cadeira', vl_preco: 620.00 }); console.log(' POST /item ->', segundo.status, segundo.texto); const lista = await pedir('GET', '/item'); console.log(' GET /item ->', lista.status, lista.texto); const um = await pedir('GET', '/item/' + id); console.log(' GET /item/' + id + ' ->', um.status, um.texto); const inexistente = await pedir('GET', '/item/9999'); console.log(' GET /item/9999 ->', inexistente.status, inexistente.texto); const invalido = await pedir('GET', '/item/abc'); console.log(' GET /item/abc ->', invalido.status, invalido.texto); console.log(' o servico recusou ANTES de consultar: id nao e inteiro, e o'); console.log(' banco nem chegou a receber a query.'); const alterado = await pedir('PUT', '/item/' + id, { nm_item: 'monitor 4k', vl_preco: 1250.00 }); console.log(' PUT /item/' + id + ' ->', alterado.status, alterado.texto); const apagado = await pedir('DELETE', '/item/' + id); console.log(' DELETE /item/' + id + ' ->', apagado.status, JSON.stringify(apagado.texto)); const depois = await pedir('GET', '/item'); console.log(' GET /item ->', depois.status, depois.texto); console.log(' o item apagado sumiu da lista: o DELETE gravou no banco de verdade.'); console.log(''); console.log('--- o que cada camada sabe, e o que ela NAO sabe ---'); console.log(' rota | sabe metodo HTTP e status | NAO sabe SQL'); console.log(' servico | sabe a regra do negocio | NAO sabe MySQL nem requisicao'); console.log(' repositorio | sabe SQL | NAO sabe que existe HTTP'); console.log(''); console.log('--- a prova de que a separacao e real ---'); console.log('o repositorio tem', Object.keys(repositorio).length, 'metodo(s), todos com `?`:'); for (const nome of Object.keys(repositorio)) { console.log(' ' + nome.padEnd(16) + ' -> ' + repositorio[nome].length + ' parametro(s)'); } console.log(''); // A verificacao nao e uma promessa: o exemplo le o proprio codigo-fonte e // conta as ocorrencias. Se alguem adicionar um `+` com valor de fora, esta // contagem muda e a aula deixa de bater com o exemplo. const fonte = __filename; const { readFileSync } = require('node:fs'); const texto = readFileSync(fonte, 'utf8'); const corpoRepositorio = texto.slice(texto.indexOf('criarRepositorio'), texto.indexOf('criarServico')); const comMais = (corpoRepositorio.match(/\+ /g) || []).length; console.log('varredura do codigo da camada de repositorio:'); console.log(' ocorrencias de `+ ` (concatenacao):', comMais); console.log(' `?` em consultas: ', (corpoRepositorio.match(/\?/g) || []).length); console.log(''); console.log('MEDIDO: zero concatenacao e oito `?` na camada de repositorio. O'); console.log('`ORDER BY id` e literal escrito no texto do SQL, sem `+` e sem valor'); console.log('de fora. Todo dado que vem do cliente entra por `?`.'); console.log(''); console.log('E o `id` de /item/9999 nunca chegou ao banco, porque o servico devolveu'); console.log('400 antes de chamar o repositorio. Isso e regra de negocio'); console.log('escrita uma vez, e nao em cada rota.'); } finally { await new Promise((resolve) => servidor.close(resolve)); await conexao.query('DROP TABLE IF EXISTS tb_d16_item'); await conexao.end(); console.log(''); console.log('servidor encerrado com close(); tabela de teste removida.'); } } main().catch((erro) => { console.error('falhou:', erro.code || erro.name, '-', erro.message); process.exit(1); });
Saída real
servidor no ar em http://127.0.0.1:34531
--- o ciclo CRUD inteiro, contra a API montada ---
POST /item -> 201 {"id":1,"nm_item":"monitor","vl_preco":890}
POST /item -> 201 {"id":2,"nm_item":"cadeira","vl_preco":620}
GET /item -> 200 [{"id":1,"nm_item":"monitor","vl_preco":"890.00"},{"id":2,"nm_item":"cadeira","vl_preco":"620.00"}]
GET /item/1 -> 200 {"id":1,"nm_item":"monitor","vl_preco":"890.00"}
GET /item/9999 -> 404 {"erro":"item nao encontrado"}
GET /item/abc -> 400 {"erro":"id precisa ser um numero inteiro positivo"}
o servico recusou ANTES de consultar: id nao e inteiro, e o
banco nem chegou a receber a query.
PUT /item/1 -> 200 {"id":1,"nm_item":"monitor 4k","vl_preco":"1250.00"}
DELETE /item/1 -> 204 ""
GET /item -> 200 [{"id":2,"nm_item":"cadeira","vl_preco":"620.00"}]
o item apagado sumiu da lista: o DELETE gravou no banco de verdade.
--- o que cada camada sabe, e o que ela NAO sabe ---
rota | sabe metodo HTTP e status | NAO sabe SQL
servico | sabe a regra do negocio | NAO sabe MySQL nem requisicao
repositorio | sabe SQL | NAO sabe que existe HTTP
--- a prova de que a separacao e real ---
o repositorio tem 5 metodo(s), todos com `?`:
listar -> 0 parametro(s)
buscarPorId -> 1 parametro(s)
criar -> 1 parametro(s)
alterar -> 2 parametro(s)
apagar -> 1 parametro(s)
varredura do codigo da camada de repositorio:
ocorrencias de `+ ` (concatenacao): 0
`?` em consultas: 8
MEDIDO: zero concatenacao e oito `?` na camada de repositorio. O
`ORDER BY id` e literal escrito no texto do SQL, sem `+` e sem valor
de fora. Todo dado que vem do cliente entra por `?`.
E o `id` de /item/9999 nunca chegou ao banco, porque o servico devolveu
400 antes de chamar o repositorio. Isso e regra de negocio
escrita uma vez, e nao em cada rota.
servidor encerrado com close(); tabela de teste removida.
Revisão e o que vem no 3º trimestre
Revisão e o que vem no 3º trimestre
Um checklist de revisão não pode ser uma lista de dúvidas no texto. Cada item
vira um teste que roda contra o banco de verdade — e o exemplo sai com
"N de N conferidos" ou com a lista do que falhou.
Revisar o trimestre é isso: não reler as aulas, e sim rodar cada afirmação contra
o banco e ver se ela ainda vale.
Os seis itens, e o que cada um mediu
| item | o que a medição provou |
|---|---|
1. [linhas, campos] | query devolve 2 posições; usar o primeiro sem [0] dá undefined sem erro |
| 2. Idempotência | duas execuções seguidas dão o mesmo resultado; a limpeza no início impede acúmulo |
3. ? protege | payload como parâmetro devolve 0 linha; concatenado devolve todas |
| 4. Transação | o rollback desfaz o grupo; o TRUNCATE não é desfeito |
| 5. Servidor | listen(0) devolveu 43453; close() resolveu em menos de 2s |
| 6. Tipos | DECIMAL volta string, TRUE volta 1, NULL volta null |
O item 1 é o que mais reprova, e a medição mostra por quê:
O item 3 é o mais importante, porque é o mesmo dado com uma caractere de
diferença:
O item 5 é o que impede o travamento:
O que este checklist não cobre
Fica para o 3º trimestre, e vale dizer em voz alta:
- autenticação e senha com hash — nenhum item aqui protege o acesso
- testes automáticos — o portão roda os exemplos, mas não há suíte de casos
de uso com bancos semeados em vários estados
- deploy — nada aqui sobe isso em nenhum servidor
- migrações — as tabelas são criadas no próprio exemplo; em produção elas
precisam de histórico versionado
A continuação não é "fazer o mesmo com mais coisa": é o mesmo caminho com as
etapas que faltavam. O próximo trimestre começa exatamente onde esta lista para,
e a conexão, que já se abre e se fecha, vai precisar saber quem está
chamando antes de devolver uma linha.
O que levar do trimestre, uma frase por tema
| dia | a frase |
|---|---|
| 1–2 | o Node é um processo do sistema operacional, não uma janela |
| 3 | CommonJS e ESM são dois sistemas, com cache no primeiro |
| 4 | fs lê arquivo; a versão assíncrona precisa de await |
| 5–6 | HTTP é texto em volta de sockets; rota é um mapa |
| 7 | o corpo da requisição chega em pedaços, e tem limite |
| 8–9 | o banco é um processo lá fora; query devolve dois valores |
| 10 | UPDATE muda dado, ALTER muda estrutura |
| 11 | chave estrangeira impede dado órfão; LEFT JOIN preserva |
| 12 | execute PREPARA; e o [linhas, campos] é a pegadinha |
| 13 | o ? protege conteúdo, não sintaxe: coluna é lista fechada |
| 14 | transação é da conexão; TRUNCATE faz commit implícito |
| 15 | 201 cria, 204 apaga sem corpo, 409 conflita |
| 16 | rota, serviço e repositório: cada um sabe o que o outro não sabe |
O que aprendi, na frase mais curta possível: o Node é um processo que precisa
subir, o MySQL é outro processo que precisa estar no ar, e entre os dois há uma
conexão que precisa ser aberta, fechada e transacionada. Tudo o mais — rota,
status, camada — é consequência de que esse caminho existe.
Os erros comuns, e de onde eles vêm
Cinco erros aparecem em toda turma, e quatro deles são o mesmo bug em formas
diferentes:
- Usar o resultado do
SELECTsem o[0]— devolveundefinedsem erro. - Concatenar valor no SQL — funciona até o cliente escrever uma aspa.
- Falta o
close()nofinally— o processo segura o event loop e trava. - Falta o
rollbacknocatch— a primeira linha da transação fica gravada. - Esquecer
CREATE TABLE IF NOT EXISTS— o exemplo funciona uma vez e
quebra na segunda.
Os quatro primeiros são a mesma coisa: **uma etapa do caminho que o código não
fez**. Por isso a organização de código que resolve os cinco ao mesmo tempo é a
do dia 16 — camadas com uma responsabilidade cada, e a montagem num único lugar
onde o try/catch/finally mora.
O checklist como código, não como texto
O ponto da aula está no formato. Um checklist escrito em prosa depende de alguém
lembrar de conferir. Um checklist em código falha sozinho — e o próximo
exemplo do 3º trimestre, rodando contra um banco diferente, descobre na hora o
que quebrou.
É a mesma ideia do portão que valida estas aulas: ele não confia que o material
está certo, ele roda e confere.
Exemplo
'use strict'; // Exemplo da aula 2 do dia 16: a revisao do trimestre, rodada como codigo. // // A ideia e que um checklist de revisao nao pode ser uma lista de duvidas no texto: // cada item vira um teste que roda contra o banco de verdade, e o exemplo sai // com "N de N conferidos" ou com a lista do que falhou. // // Os itens abaixo sao os erros que o proprio trimestre encontrou em execucao: // // 1. `query` devolve [linhas, campos] — usar o primeiro valor direto da // undefined, sem erro nenhum // 2. CREATE TABLE IF NOT EXISTS + limpeza no COMECO, nunca no fim // 3. todo valor de fora entra por `?`; a concatenacao fica so com literal // 4. transacao: rollback no catch, e TRUNCATE/CREATE fazem commit implicito // 5. servidor: listen(0), esperar 'listening', close() no finally // 6. DECIMAL volta string, e por isso muda a forma como o JSON sai // // Cada secao mede um item e imprime o que encontrou. Nada aqui e declarado // "correto" sem ter rodado. const http = require('node:http'); const { createConnection } = require('mysql2/promise'); // O placar: cada item da revisao soma um ponto se passar. const resultado = []; const conferir = (nome, passou, detalhe) => { resultado.push({ nome, passou, detalhe }); console.log((passou ? ' OK ' : ' FALHA') + ' ' + nome); if (detalhe) console.log(' ' + detalhe); }; async function main() { const conexao = 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, }); try { await conexao.query('DROP TABLE IF EXISTS tb_d16a2_revisao'); await conexao.query(` CREATE TABLE tb_d16a2_revisao ( id INT AUTO_INCREMENT PRIMARY KEY, nm_item VARCHAR(40) NOT NULL, vl_preco DECIMAL(10,2) NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 `); console.log('--- revisao do 2º trimestre, rodada contra o banco ---'); console.log(''); // ================================================================= // 1. [linhas, campos] // ================================================================= console.log('=== 1. `query` devolve [linhas, campos] ==='); await conexao.query('DELETE FROM tb_d16a2_revisao'); await conexao.query('INSERT INTO tb_d16a2_revisao (nm_item, vl_preco) VALUES (?, ?)', ['teclado', 150]); const resultadoCru = await conexao.query('SELECT nm_item FROM tb_d16a2_revisao'); conferir('query devolve um array com 2 posicoes', Array.isArray(resultadoCru) && resultadoCru.length === 2, 'o que voltou: ' + (Array.isArray(resultadoCru) ? resultadoCru.length + ' posicao(oes)' : typeof resultadoCru)); const [linhas, campos] = resultadoCru; conferir('o primeiro valor e a lista de linhas', Array.isArray(linhas), 'typeof linhas = ' + typeof linhas); conferir('o segundo valor sao os metadados', campos !== undefined, 'campos tem ' + (campos ? campos.length : 0) + ' coluna(s)'); // O erro classico: pegar o primeiro valor sem o [0]. const semIndice = await conexao.query('SELECT nm_item FROM tb_d16a2_revisao'); const errado = semIndice[0].nm_item; conferir('usar semIndice[0].nm_item devolve undefined (o bug classico)', errado === undefined, 'valor obtido: ' + errado + ' <- o programa continua rodando, e e por isso que o bug e silencioso'); conferir('com o [0], o valor aparece', linhas[0].nm_item === 'teclado', 'linhas[0].nm_item = ' + linhas[0].nm_item); // ================================================================= // 2. idempotencia: rodar duas vezes tem que dar o mesmo resultado // ================================================================= console.log(''); console.log('=== 2. o exemplo e idempotente? ==='); const rodar = async () => { await conexao.query('DELETE FROM tb_d16a2_revisao'); await conexao.query('INSERT INTO tb_d16a2_revisao (nm_item, vl_preco) VALUES (?, ?), (?, ?)', ['teclado', 150, 'mouse', 60]); const [r] = await conexao.query('SELECT nm_item, vl_preco FROM tb_d16a2_revisao ORDER BY id'); return r; }; const primeiraVez = await rodar(); const segundaVez = await rodar(); conferir('duas execucoes seguidas dao o mesmo resultado', JSON.stringify(primeiraVez) === JSON.stringify(segundaVez), '1a: ' + JSON.stringify(primeiraVez) + ' | 2a: ' + JSON.stringify(segundaVez)); const [conta1] = await conexao.query('SELECT COUNT(*) AS n FROM tb_d16a2_revisao'); conferir('a limpeza acontece no COMECO, entao a contagem nao acumula', Number(conta1[0].n) === 2, 'linhas na tabela apos duas execucoes: ' + conta1[0].n + ' (se a limpeza fosse no fim, seriam 4)'); // ================================================================= // 3. parametro nao concatena // ================================================================= console.log(''); console.log('=== 3. o payload de ataque nao vira sintaxe ==='); const ataque = "' OR '1'='1"; const [comParametro] = await conexao.query( 'SELECT nm_item FROM tb_d16a2_revisao WHERE nm_item = ?', [ataque]); conferir('payload como parametro devolve 0 linha', comParametro.length === 0, "login " + JSON.stringify(ataque) + " procurou um usuario chamado isso, e nao achou"); // O mesmo payload concatenado, contra outra tabela, para o contraste. const [comConcatenacao] = await conexao.query( 'SELECT nm_item FROM tb_d16a2_revisao WHERE nm_item = ' + "'" + ataque + "'"); conferir('payload concatenado devolve TODAS as linhas (o estrago)', comConcatenacao.length === 2, 'mesma frase, mesmo dado, so mudou o `+` por `?`: ' + comConcatenacao.length + ' linha(s)'); // ================================================================= // 4. transacao // ================================================================= console.log(''); console.log('=== 4. rollback desfaz o grupo inteiro ==='); await conexao.query('DELETE FROM tb_d16a2_revisao'); await conexao.query('INSERT INTO tb_d16a2_revisao (id, nm_item, vl_preco) VALUES (1, ?, ?)', ['semente', 1]); const [antes4] = await conexao.query('SELECT COUNT(*) AS n FROM tb_d16a2_revisao'); await conexao.beginTransaction(); try { await conexao.query('INSERT INTO tb_d16a2_revisao (nm_item, vl_preco) VALUES (?, ?)', ['dentro', 2]); await conexao.query('INSERT INTO tb_d16a2_revisao (id, nm_item, vl_preco) VALUES (1, ?, ?)', ['erro', 0]); } catch (erro) { console.log(' (o erro esperado veio: ' + erro.code + ')'); await conexao.rollback(); } const [depois4] = await conexao.query('SELECT COUNT(*) AS n FROM tb_d16a2_revisao'); conferir('a linha gravada antes do erro desapareceu', Number(antes4[0].n) === Number(depois4[0].n), 'antes: ' + antes4[0].n + ' | depois do rollback: ' + depois4[0].n + ' (a "dentro" sumiu)'); console.log(''); console.log(' e o TRUNCATE, que faz commit implicito:'); await conexao.query('INSERT INTO tb_d16a2_revisao (nm_item, vl_preco) VALUES (?, ?), (?, ?)', ['a', 1, 'b', 2]); await conexao.beginTransaction(); await conexao.query('TRUNCATE TABLE tb_d16a2_revisao'); await conexao.rollback(); const [depoisTrunc] = await conexao.query('SELECT COUNT(*) AS n FROM tb_d16a2_revisao'); conferir('TRUNCATE NAO e desfeito pelo rollback (commit implicito)', Number(depoisTrunc[0].n) === 0, 'linhas depois do rollback do TRUNCATE: ' + depoisTrunc[0].n + ' (continua vazio: o TRUNCATE gravou fora)'); // ================================================================= // 5. servidor: listen(0), close() no finally // ================================================================= console.log(''); console.log('=== 5. o servidor sobe e desce ==='); const servidor = http.createServer((req, res) => { res.writeHead(200, { 'Content-Type': 'application/json' }); res.end('{"ok":true}'); }); await new Promise((resolve) => servidor.listen(0, '127.0.0.1', resolve)); const { port } = servidor.address(); conferir('listen(0) devolveu uma porta livre, e nao a 3000', typeof port === 'number' && port > 0 && port !== 3000, 'porta desta execucao: ' + port + ' (muda toda vez — e por isso que o exemplo nao fixa 3000)'); const resposta = await fetch('http://127.0.0.1:' + port + '/'); conferir('o servidor respondeu de verdade', resposta.status === 200, 'status: ' + resposta.status + ' | corpo: ' + await resposta.text()); // O close() tem que resolver. Se o servidor nao fechasse, este exemplo // ficaria pendurado e o portao esperaria ate o timeout. const fechou = await Promise.race([ new Promise((resolve) => servidor.close(() => resolve(true))), new Promise((resolve) => setTimeout(() => resolve(false), 2000)), ]); conferir('close() no finally devolveu (o processo nao fica pendurado)', fechou === true, 'o servidor respondeu ao close() em menos de 2 segundos'); // ================================================================= // 6. os tipos que chegam no Node // ================================================================= console.log(''); console.log('=== 6. os tipos que o driver devolve ==='); await conexao.query('DROP TABLE IF EXISTS tb_d16a2_tipos'); await conexao.query(` CREATE TABLE tb_d16a2_tipos ( id INT PRIMARY KEY, nm_texto VARCHAR(10), vl_dec DECIMAL(10,2), vl_real DOUBLE, dt_dia DATE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 `); await conexao.query( "INSERT INTO tb_d16a2_tipos VALUES (1, 'texto', 10.50, 1.5, '2026-05-04'), (2, NULL, NULL, TRUE, NULL)"); const [tipos] = await conexao.query( 'SELECT id, nm_texto, vl_dec, vl_real, dt_dia FROM tb_d16a2_tipos ORDER BY id'); conferir('VARCHAR volta string', typeof tipos[0].nm_texto === 'string', 'nm_texto = ' + JSON.stringify(tipos[0].nm_texto) + ' (' + typeof tipos[0].nm_texto + ')'); conferir('DECIMAL volta string, nao number', typeof tipos[0].vl_dec === 'string', 'vl_dec = ' + JSON.stringify(tipos[0].vl_dec) + ' (' + typeof tipos[0].vl_dec + ') — o tipo e exato'); conferir('DOUBLE volta number', typeof tipos[0].vl_real === 'number', 'vl_real = ' + tipos[0].vl_real + ' (' + typeof tipos[0].vl_real + ')'); conferir('TRUE volta 1, nao true', tipos[1].vl_real === 1, 'vl_real com TRUE = ' + tipos[1].vl_real + ' (' + typeof tipos[1].vl_real + ') — o MySQL guarda bool como TINYINT'); conferir('NULL volta null', tipos[1].nm_texto === null, 'nm_texto nulo = ' + tipos[1].nm_texto + ' (nao "null", nao 0)'); conferir('DOUBLE guarda fracao, DECIMAL nao', tipos[0].vl_real === 1.5 && tipos[0].vl_dec === '10.50', 'vl_real = ' + tipos[0].vl_real + ' | vl_dec = ' + JSON.stringify(tipos[0].vl_dec)); // ================================================================= // o placar // ================================================================= const passaram = resultado.filter((r) => r.passou).length; console.log(''); console.log('=== o placar ==='); console.log(' ' + passaram + ' de ' + resultado.length + ' conferidos passaram'); if (passaram === resultado.length) { console.log(' todos os itens do checklist passaram nesta execucao.'); } else { console.log(' itens que FALHARAM:'); for (const r of resultado.filter((x) => !x.passou)) console.log(' - ' + r.nome); } console.log(''); console.log('O que este checklist NAO cobre, e fica para o 3º trimestre:'); console.log(' autenticacao e senha com hash (nenhum item aqui protege o acesso)'); console.log(' testes automaticos: aqui o portao roda os exemplos, mas nao ha'); console.log(' suite de casos de uso com bancos semeados em varios estados'); console.log(' deploy: nada aqui sobe isso em nenhum servidor'); console.log(' migracoes: as tabelas sao criadas no proprio exemplo; em producao'); console.log(' elas precisam de historico versionado'); console.log(''); console.log('E o que voce deve levar do 2º trimestre, em uma frase por tema:'); console.log(' dia 1-2 o Node e um processo do sistema operacional, nao uma janela'); console.log(' dia 3 CommonJS e ESM sao dois sistemas, com cache no primeiro'); console.log(' dia 4 `fs` le arquivo; a versao assincrona precisa de `await`'); console.log(' dia 5-6 HTTP e texto em volta de sockets; rota e um mapa'); console.log(' dia 7 o corpo da requisicao chega em pedacos, e tem limite'); console.log(' dia 8-9 o banco e um processo la fora; `query` devolve dois valores'); console.log(' dia 10 `UPDATE` muda dado, `ALTER` muda estrutura'); console.log(' dia 11 chave estrangeira impede dado orfao; `LEFT JOIN` preserva'); console.log(' dia 12 `execute` PREPARA; e o `[linhas, campos]` e a pegadinha'); console.log(' dia 13 o `?` protege conteudo, nao sintaxe: coluna e lista fechada'); console.log(' dia 14 transacao e da conexao; `TRUNCATE` faz commit implicito'); console.log(' dia 15 201 cria, 204 apaga sem corpo, 409 conflita'); console.log(' dia 16 rota, servico e repositorio: cada um sabe o que o outro nao sabe'); } finally { await conexao.query('DROP TABLE IF EXISTS tb_d16a2_revisao'); await conexao.query('DROP TABLE IF EXISTS tb_d16a2_tipos'); await conexao.end(); console.log(''); console.log('tabelas de teste removidas; conexao encerrada com end().'); } } main().catch((erro) => { console.error('falhou:', erro.code || erro.name, '-', erro.message); process.exit(1); });
Saída real
--- revisao do 2º trimestre, rodada contra o banco ---
=== 1. `query` devolve [linhas, campos] ===
OK query devolve um array com 2 posicoes
o que voltou: 2 posicao(oes)
OK o primeiro valor e a lista de linhas
typeof linhas = object
OK o segundo valor sao os metadados
campos tem 1 coluna(s)
OK usar semIndice[0].nm_item devolve undefined (o bug classico)
valor obtido: undefined <- o programa continua rodando, e e por isso que o bug e silencioso
OK com o [0], o valor aparece
linhas[0].nm_item = teclado
=== 2. o exemplo e idempotente? ===
OK duas execucoes seguidas dao o mesmo resultado
1a: [{"nm_item":"teclado","vl_preco":"150.00"},{"nm_item":"mouse","vl_preco":"60.00"}] | 2a: [{"nm_item":"teclado","vl_preco":"150.00"},{"nm_item":"mouse","vl_preco":"60.00"}]
OK a limpeza acontece no COMECO, entao a contagem nao acumula
linhas na tabela apos duas execucoes: 2 (se a limpeza fosse no fim, seriam 4)
=== 3. o payload de ataque nao vira sintaxe ===
OK payload como parametro devolve 0 linha
login "' OR '1'='1" procurou um usuario chamado isso, e nao achou
OK payload concatenado devolve TODAS as linhas (o estrago)
mesma frase, mesmo dado, so mudou o `+` por `?`: 2 linha(s)
=== 4. rollback desfaz o grupo inteiro ===
(o erro esperado veio: ER_DUP_ENTRY)
OK a linha gravada antes do erro desapareceu
antes: 1 | depois do rollback: 1 (a "dentro" sumiu)
e o TRUNCATE, que faz commit implicito:
OK TRUNCATE NAO e desfeito pelo rollback (commit implicito)
linhas depois do rollback do TRUNCATE: 0 (continua vazio: o TRUNCATE gravou fora)
=== 5. o servidor sobe e desce ===
OK listen(0) devolveu uma porta livre, e nao a 3000
porta desta execucao: 45553 (muda toda vez — e por isso que o exemplo nao fixa 3000)
OK o servidor respondeu de verdade
status: 200 | corpo: {"ok":true}
OK close() no finally devolveu (o processo nao fica pendurado)
o servidor respondeu ao close() em menos de 2 segundos
=== 6. os tipos que o driver devolve ===
OK VARCHAR volta string
nm_texto = "texto" (string)
OK DECIMAL volta string, nao number
vl_dec = "10.50" (string) — o tipo e exato
OK DOUBLE volta number
vl_real = 1.5 (number)
OK TRUE volta 1, nao true
vl_real com TRUE = 1 (number) — o MySQL guarda bool como TINYINT
OK NULL volta null
nm_texto nulo = null (nao "null", nao 0)
OK DOUBLE guarda fracao, DECIMAL nao
vl_real = 1.5 | vl_dec = "10.50"
=== o placar ===
20 de 20 conferidos passaram
todos os itens do checklist passaram nesta execucao.
O que este checklist NAO cobre, e fica para o 3º trimestre:
autenticacao e senha com hash (nenhum item aqui protege o acesso)
testes automaticos: aqui o portao roda os exemplos, mas nao ha
suite de casos de uso com bancos semeados em varios estados
deploy: nada aqui sobe isso em nenhum servidor
migracoes: as tabelas sao criadas no proprio exemplo; em producao
elas precisam de historico versionado
E o que voce deve levar do 2º trimestre, em uma frase por tema:
dia 1-2 o Node e um processo do sistema operacional, nao uma janela
dia 3 CommonJS e ESM sao dois sistemas, com cache no primeiro
dia 4 `fs` le arquivo; a versao assincrona precisa de `await`
dia 5-6 HTTP e texto em volta de sockets; rota e um mapa
dia 7 o corpo da requisicao chega em pedacos, e tem limite
dia 8-9 o banco e um processo la fora; `query` devolve dois valores
dia 10 `UPDATE` muda dado, `ALTER` muda estrutura
dia 11 chave estrangeira impede dado orfao; `LEFT JOIN` preserva
dia 12 `execute` PREPARA; e o `[linhas, campos]` e a pegadinha
dia 13 o `?` protege conteudo, nao sintaxe: coluna e lista fechada
dia 14 transacao e da conexao; `TRUNCATE` faz commit implicito
dia 15 201 cria, 204 apaga sem corpo, 409 conflita
dia 16 rota, servico e repositorio: cada um sabe o que o outro nao sabe
tabelas de teste removidas; conexao encerrada com end().