Dia 16 — Projeto do trimestre: API com MySQL

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

Projeto do trimestre: API com MySQL

Aula 1

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.

camadanome em inglêssabenão sabe
rotacontrollermétodo HTTP e statusSQL
serviçoservicea regra do negócioMySQL e requisição
repositóriorepositorySQLque 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.
Aula 2

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

itemo que a medição provou
1. [linhas, campos]query devolve 2 posições; usar o primeiro sem [0] dá undefined sem erro
2. Idempotênciaduas execuções seguidas dão o mesmo resultado; a limpeza no início impede acúmulo
3. ? protegepayload como parâmetro devolve 0 linha; concatenado devolve todas
4. Transaçãoo rollback desfaz o grupo; o TRUNCATE não é desfeito
5. Servidorlisten(0) devolveu 43453; close() resolveu em menos de 2s
6. TiposDECIMAL 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

diaa frase
1–2o Node é um processo do sistema operacional, não uma janela
3CommonJS e ESM são dois sistemas, com cache no primeiro
4fs lê arquivo; a versão assíncrona precisa de await
5–6HTTP é texto em volta de sockets; rota é um mapa
7o corpo da requisição chega em pedaços, e tem limite
8–9o banco é um processo lá fora; query devolve dois valores
10UPDATE muda dado, ALTER muda estrutura
11chave estrangeira impede dado órfão; LEFT JOIN preserva
12execute PREPARA; e o [linhas, campos] é a pegadinha
13o ? protege conteúdo, não sintaxe: coluna é lista fechada
14transação é da conexão; TRUNCATE faz commit implícito
15201 cria, 204 apaga sem corpo, 409 conflita
16rota, 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:

  1. Usar o resultado do SELECT sem o [0] — devolve undefined sem erro.
  2. Concatenar valor no SQL — funciona até o cliente escrever uma aspa.
  3. Falta o close() no finally — o processo segura o event loop e trava.
  4. Falta o rollback no catch — a primeira linha da transação fica gravada.
  5. 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().