Dia 15 — CRUD completo com HTTP e MySQL

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

CRUD completo com HTTP e MySQL

Aula 1

Listar e buscar

Servidor HTTP falando com o MySQL

Até aqui o MySQL foi chamado por linha de comando. Agora ele responde a uma

requisição HTTP — e o servidor precisa ser capaz de terminar, senão o exemplo

trava.

O conjunto de rotas que escreve, lê, altera e apaga registro em uma tabela se

chama CRUD, e é o que a aula 2 completa. Esta aula monta a parte de leitura: um

GET que lista do banco e outro que busca registro por id.

O que a porta mostra

A porta muda a cada execução. Isso não é defeito: é o listen(0). Se o

exemplo mostrasse uma porta fixa, ele pareceria quebrado na sua máquina — a 3000

pode estar ocupada, e aí o exemplo falharia por motivo aleatório.

listen(0), esperar o 'listening', fechar no finally

Três coisas, e as três são obrigatórias:

await new Promise((resolve) => servidor.listen(0, '127.0.0.1', resolve));
const { port } = servidor.address();
// ... requisições ...
await new Promise((resolve) => servidor.close(resolve));
passopor quê
listen(0)pede uma porta livre ao sistema, nunca fixa
esperar o listeningo port só existe depois que o servidor subiu
close() no finallysem ele o processo fica segurando o event loop

O finally é o que impede o travamento. Se a requisição falhar e o close()

estivesse fora dele, o servidor continuaria no ar e a validação esperaria até o

timeout — sem mensagem de erro, só demora.

O 404 aparece de verdade

O servidor tem duas rotas: /consulta, que consulta o banco, e qualquer outra

coisa, que não existe. O exemplo pede /nao-existe e mede:

Isto importa para a honestidade da aula: um exemplo que responde 200 para

qualquer caminho desmente o próprio texto que fala em 404. Aqui o 404 é

medido, não descrito.

const servidor = http.createServer(async (req, res) => {
  const rota = rotas[req.url];
  if (!rota) {
    res.writeHead(404, { 'Content-Type': 'application/json; charset=utf-8' });
    res.end(JSON.stringify({ erro: 'rota nao encontrada' }));
    return;
  }
  await rota(res);
});

O return depois do res.end() não é decoração: sem ele a execução cairia no

await rota(res) com rota sendo undefined, e o erro apareceria como

TypeError em vez de um 404 limpo.

Uma rota só não faz CRUD: são duas, e elas têm nomes diferentes.

rotamétodoo que faz
/alunoGET com bancolistar do banco, sem filtro
/aluno?turma=3GET com bancolistar filtrado, pelo WHERE
/aluno/7GET por idbuscar registro, WHERE id = ?

O listar filtrado é o mesmo SELECT com o WHERE montado a partir do

parâmetro — e é o ? do URLSearchParams que decide, nunca o texto do

cliente. Sem o filtro, o WHERE some da frase: a lista inteira volta.

O GET por id tem uma resposta que a lista não tem: quando o id não existe, o

SELECT devolve linhas vazio — nunca undefined. Aí o status é 404, e a

mensagem é a de registro não encontrado. É o 404 no banco: não é "a rota

não existe", é "a rota existe e o que ela pede não está lá". A distinção

importa porque o cliente trata os dois de formas diferentes — uma é erro de

digitação do endereço, a outra é erro de identificador.

O JSON da lista é JSON.stringify(linhas), e ele é a resposta do `GET com

banco. linhas já é um array de objetos e o JSON.stringify` não aceita

undefined dentro — por isso o SELECT que monta a lista precisa tratar o

NULL (com COALESCE) antes, e não depois.

console.error não chega na página

} catch (erro) {
  console.error('falha na requisicao: ' + erro.code + ' - ' + erro.message);
  console.log('  erro na requisicao:', erro.code, '-', erro.message);
  process.exitCode = 1;
}

console.error vai para o stderr, e o stderr não é capturado pela página.

O par acima é o que garante que o terminal mostre o erro e a página também.

Quando a aula ensina erro, os dois precisam aparecer.

Top-level await não existe aqui

O arquivo é .js e o package.json diz commonjs, então tudo está dentro de

async function main(). A aula de ESM do dia 3 mostra o outro jeito — e lá o

await no topo da arquivo é legal.

Exemplo

'use strict';

// Exemplo da aula 1 do dia 15: o servidor HTTP falando com o MySQL.
//
// Top-level `await` nao existe em CommonJS: o arquivo e `.js` e o
// `package.json` diz `commonjs`, entao tudo precisa ficar dentro de uma
// `async function`. A aula de ESM (dia 3) mostra o outro jeito.
//
// As quatro coisas que o exemplo precisa acertar, e que o portao exige:
//   1. `listen(0)` — porta livre pedida ao sistema, nunca 3000 fixa
//   2. esperar o 'listening' antes de chamar, com `await new Promise`
//   3. `server.close()` no `finally`, sempre, mesmo se a requisicao falhar
//   4. uma linha de `console.log` de contexto ANTES de cada erro

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

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,
  });

  // A tabela que a rota consulta. `IF NOT EXISTS` porque o exemplo roda varias
  // vezes seguidas — rodar duas vezes tem que dar o mesmo resultado.
  await conexao.query(`
    CREATE TABLE IF NOT EXISTS tb_d15a1_http (
      id     INT AUTO_INCREMENT PRIMARY KEY,
      nm_rota VARCHAR(40) NOT NULL
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
  `);
  await conexao.query('DELETE FROM tb_d15a1_http');
  await conexao.query('INSERT INTO tb_d15a1_http (nm_rota) VALUES (?), (?)',
    ['/consulta', '/nao-existe']);

  // Um roteador de verdade: o caminho decide a resposta. E assim que o 404
  // aparece DE VERDADE, e nao apenas descrito no texto — o exemplo anterior
  // respondia 200 para qualquer caminho, e o `status: 200` do `/nao-existe`
  // desmentia a propria frase do comentario.
  const rotas = {
    '/consulta': async (res) => {
      const [linhas] = await conexao.query(
        'SELECT COUNT(*) AS total FROM tb_d15a1_http');
      res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
      res.end(JSON.stringify({ total: linhas[0].total }));
    },
    '/rota': (res) => {
      res.writeHead(404, { 'Content-Type': 'application/json; charset=utf-8' });
      res.end(JSON.stringify({ erro: 'rota nao encontrada' }));
    },
  };

  const servidor = http.createServer(async (req, res) => {
    const rota = rotas[req.url];
    if (!rota) {
      res.writeHead(404, { 'Content-Type': 'application/json; charset=utf-8' });
      res.end(JSON.stringify({ erro: 'rota nao encontrada' }));
      return;
    }
    await rota(res);
  });

  // `listen(0)`: a porta 3000 pode estar ocupada na maquina de quem roda, e o
  // exemplo passaria a falhar por motivo aleatorio. Zero pede uma livre.
  await new Promise((resolve) => servidor.listen(0, '127.0.0.1', resolve));
  const { port } = servidor.address();

  try {
    console.log('servidor no ar em http://127.0.0.1:' + port);

    const resposta = await fetch(`http://127.0.0.1:${port}/consulta`);
    const corpo = await resposta.json();

    console.log('status:', resposta.status);
    console.log('corpo:', JSON.stringify(corpo));

    // O mesmo servidor, agora em um caminho que ele nao conhece: e assim que o
    // 404 do servidor aparece de verdade, e nao descrito no texto.
    const perdida = await fetch(`http://127.0.0.1:${port}/nao-existe`);
    console.log('rota desconhecida, status:', perdida.status);
    console.log('rota desconhecida, corpo:', await perdida.text());
  } catch (erro) {
    // `console.error` vai para o stderr, que NAO chega na pagina. O par abaixo
    // e o que o CONTRATO.md exige: o terminal mostra o erro, a pagina tambem.
    console.error('falha na requisicao: ' + erro.code + ' - ' + erro.message);
    console.log('  erro na requisicao:', erro.code, '-', erro.message);
    process.exitCode = 1;
  } finally {
    // O `finally` e o que impede o processo de travar: sem ele o servidor fica
    // segurando o event loop e o portao espera ate o timeout.
    await new Promise((resolve) => servidor.close(resolve));
    console.log('servidor encerrado com close().');
    await conexao.end();
  }
}

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:44617
status: 200
corpo: {"total":2}
rota desconhecida, status: 404
rota desconhecida, corpo: {"erro":"rota nao encontrada"}
servidor encerrado com close().
Aula 2

Criar, alterar e apagar

Criar, alterar e apagar

A aula 1 leu do banco. Esta escreve. O que muda é o status HTTP: leitura

devolve 200, criação devolve 201, e apagado devolve 204 sem corpo.

São as três escritas do CRUD, e cada uma tem um método e uma resposta:

operaçãométodostatuso que devolve
POST com bancoPOST201o registro criado, com o id novo
alterar registroPUT200o registro relido depois da alteração
alterar registroPATCH200só o que mudou, e nem toda tabela aceita
DELETE com bancoDELETE204nada — sem corpo

POST: o corpo da resposta e o status 201

201 é "criado", e é diferente de 200 porque o cliente precisa saber que aquilo

não existia antes. O corpo devolve o registro com o insertId do banco: se

devolvesse só o que o cliente mandou, o id faltaria — e o id é a única coisa

que o banco Gabriou e o cliente não.

Repare também em vl_preco: 150 (número, sem as casas decimais). Quem montou

esse corpo foi o código, com o objeto do cliente.

O 409 quando o nome já existe

A coluna tem UNIQUE KEY uk_nome, e o ER_DUP_ENTRY do driver vira 409 na

rota. O 409 é diferente do 400: o pedido está bem formado, mas conflita com

o estado atual do banco.

O 400 quando o pedido está errado

O 400 é a resposta para corpo que o servidor não sabe interpretar, e a

regra prática é: se o erro está no que o cliente mandou, a resposta é

4xx com mensagem; se está no que o servidor faz, a resposta é 5xx e

o cliente não tem nada a corrigir. validate() roda antes de qualquer

consulta, então um corpo inválido nunca chega ao banco — o affectedRows da

tentativa inválida é 0 porque nenhuma linha foi tocada.

O 200 do PUT traz o registro relido do banco

200, não 201: o recurso já existia e foi substituído. E repara no corpo:

aqui vl_preco saiu como "189.00", texto — o mesmo campo que no POST saiu

como número.

O PUT substitui o registro inteiro: o corpo precisa ter todos os campos

obrigatórios, e o que o cliente não mandar é sobrescrito com o padrão da coluna.

O PATCH é o outro método de alterar registro, e a diferença é essa: ele altera

só os campos presentes no corpo. Em MySQL não existe PATCH — é o nome do

método HTTP, e o que o banco faz por baixo é o mesmo UPDATE ... SET com as

colunas que vieram no corpo:

UPDATE t SET nm_produto = ?, vl_preco = ? WHERE id = ?;

A prática mostra que o SET é montado com lista fechada, pelo mesmo motivo do

dia 13: nome de coluna não pode ser ?, então o código escolhe quais colunas o

cliente pode mexer.

A diferença é de onde vem o corpo:

rotao corpo vem devl_preco sai como
POSTdo objeto que o cliente mandou150 (number)
PUTde um SELECT no banco, depois do update"189.00" (string)

DECIMAL volta string para não perder precisão. Duas rotas de escrita, dois

formatos de resposta — e nenhum dos dois é errado.

O 404 quando o id não existe

O affectedRows === 0 do UPDATE é o sinal de que nada mudou. Sem esse

if, a rota responderia 200 para um registro que não existe.

O 204 é sem corpo

res.writeHead(204);
res.end();

Sem JSON.stringify, sem Content-Type. Um 204 não tem corpo — mandar JSON

junto quebra a resposta no cliente. É a resposta mais curta do HTTP, e ela diz

tudo: apagar registro foi feito, e não há o que descrever.

Repetir o DELETE no mesmo id devolve 404:

E o estado final da tabela confirma que o apagado aconteceu:

O roteador: id na URL, chave na rota

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 rota = rotas[req.method + ' /' + recurso];

/item dá ['item'] e id = null; /item/7 dá ['item', '7'] e id = 7. A

chave da rota é sempre MÉTODO /item, e o id viaja como argumento — ele não

faz parte do nome da rota. O Number.isInteger(id) no if recusa /item/abc com

404 em vez de deixar o NaN chegar no SQL.

O mapa dos status

statusquando
200leu ou alterou, e há resposta
201criou, e a criação tem id novo
204apagou, e não há o que responder
400o pedido do cliente está errado
404o recurso não existe no banco
409o recurso já existe com esse nome

A resposta do CRUD é esse mapa inteiro, e ele é o contrato entre o servidor

e quem consome: o status diz o que aconteceu com criar registro, alterar

registro e apagar registro, e o corpo — quando existe — carrega o registro ou a

mensagem de erro. Um POST que devolve 200 funciona e não diz nada ao cliente

sobre o que ele acabou de criar; por isso o status 201 é o que carrega essa

informação, e o corpo devolve o id novo que só o banco podia gerar.

Exemplo

'use strict';

// Exemplo da aula 2 do dia 15: o CRUD de escrita — POST, PUT, DELETE.
//
// A aula 1 leu do banco. Esta escreve. O que muda e o status HTTP: leitura
// devolve 200, criacao devolve 201, e apagado devolve 204 SEM CORPO.
//
// Os quatro status medidos aqui nao sao opiniao de livro:
//
//   201  o recurso foi criado, e o corpo traz o que foi gravado
//   200  o recurso foi alterado, e o corpo traz o registro ja atualizado
//   204  apagado, e NAO ha corpo para mandar
//   404  o id nao existe no banco
//   400  o corpo veio sem o campo obrigatorio
//   409  o mesmo nome ja existe
//
// `console.error` vai para o stderr e nao chega na pagina: toda vez que a aula
// ensina um erro, ela pareia com um `console.log`.

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

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,
  });

  await conexao.query(`
    CREATE TABLE IF NOT EXISTS tb_d15a2_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
  `);
  // No COMECO, nunca no fim: rodar duas vezes tem que dar o mesmo resultado.
  await conexao.query('DELETE FROM tb_d15a2_item');

  // ---------------------------------------------------------------------
  // as quatro rotas de escrita
  // ---------------------------------------------------------------------
  const rotas = {
    // POST /item — cria. Responde 201, que e diferente de 200.
    'POST /item': async (req, res) => {
      const corpo = await lerCorpo(req);
      if (!corpo || !corpo.nm_item || corpo.vl_preco === undefined) {
        console.log('  POST sem nm_item ou vl_preco: recusado com 400');
        res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
        return res.end(JSON.stringify({ erro: 'nm_item e vl_preco sao obrigatorios' }));
      }
      try {
        const [gravado] = await conexao.query(
          'INSERT INTO tb_d15a2_item (nm_item, vl_preco) VALUES (?, ?)',
          [corpo.nm_item, corpo.vl_preco]
        );
        // 201 e "criado", e o corpo devolve o registro ja com o id do banco.
        // Se devolvesse so o que o cliente mandou, o id faltaria.
        res.writeHead(201, { 'Content-Type': 'application/json; charset=utf-8' });
        res.end(JSON.stringify({ id: gravado.insertId, ...corpo }));
      } catch (erro) {
        if (erro.code === 'ER_DUP_ENTRY') {
          console.log('  POST com nome repetido: recusado com 409');
          res.writeHead(409, { 'Content-Type': 'application/json; charset=utf-8' });
          return res.end(JSON.stringify({ erro: 'ja existe um item com esse nome' }));
        }
        throw erro;
      }
    },

    // PUT /item/:id — substitui o registro inteiro. 200 com o registro novo.
    'PUT /item': async (req, res, id) => {
      const corpo = await lerCorpo(req);
      if (!corpo || !corpo.nm_item || corpo.vl_preco === undefined) {
        console.log('  PUT sem nm_item ou vl_preco: recusado com 400');
        res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
        return res.end(JSON.stringify({ erro: 'nm_item e vl_preco sao obrigatorios' }));
      }
      const [mudou] = await conexao.query(
        'UPDATE tb_d15a2_item SET nm_item = ?, vl_preco = ? WHERE id = ?',
        [corpo.nm_item, corpo.vl_preco, id]
      );
      if (mudou.affectedRows === 0) {
        console.log('  PUT no id ' + id + ': nao existe no banco, 404');
        res.writeHead(404, { 'Content-Type': 'application/json; charset=utf-8' });
        return res.end(JSON.stringify({ erro: 'item nao encontrado' }));
      }
      const [atual] = await conexao.query(
        'SELECT id, nm_item, vl_preco FROM tb_d15a2_item WHERE id = ?', [id]
      );
      res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
      res.end(JSON.stringify(atual[0]));
    },

    // DELETE /item/:id — apaga. Responde 204, e um 204 NAO tem corpo.
    'DELETE /item': async (req, res, id) => {
      const [apagou] = await conexao.query('DELETE FROM tb_d15a2_item WHERE id = ?', [id]);
      if (apagou.affectedRows === 0) {
        console.log('  DELETE no id ' + id + ': nao existe no banco, 404');
        res.writeHead(404, { 'Content-Type': 'application/json; charset=utf-8' });
        return res.end(JSON.stringify({ erro: 'item nao encontrado' }));
      }
      // 204 = "pronto, nao ha mais nada". E a resposta mais curta do HTTP.
      // Mandar JSON junto com 204 quebra a resposta no cliente.
      res.writeHead(204);
      res.end();
    },
  };

  // Le o corpo da requisicao. O `for await` do `req` entrega os pedacos.
  async function lerCorpo(req) {
    const pedacos = [];
    for await (const pedaco of req) pedacos.push(pedaco);
    if (pedacos.length === 0) return null;
    try {
      return JSON.parse(Buffer.concat(pedacos).toString('utf8'));
    } catch {
      return null;
    }
  }

  const servidor = http.createServer(async (req, res) => {
    try {
      // `/item`     -> recurso 'item', sem id
      // `/item/7`   -> recurso 'item', id 7
      // O id vem da segunda parte do caminho; a chave da rota e sempre
      // `METODO /item`, e o id viaja como argumento.
      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 rota = rotas[req.method + ' /' + recurso];
      if (!rota || (id !== null && !Number.isInteger(id))) {
        res.writeHead(404, { 'Content-Type': 'application/json; charset=utf-8' });
        return res.end(JSON.stringify({ erro: 'rota nao encontrada' }));
      }
      await rota(req, res, id);
    } catch (erro) {
      console.error('erro no servidor: ' + erro.code + ' - ' + erro.message);
      console.log('  erro no servidor:', erro.code, '-', erro.message);
      res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8' });
      res.end(JSON.stringify({ erro: 'erro interno' }));
    }
  });

  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);
    const texto = await r.text();
    return { status: r.status, corpo: texto };
  };

  try {
    console.log('servidor no ar em ' + base);
    console.log('');
    console.log('--- POST /item: o que devolve 201 ---');
    const criado = await pedir('POST', '/item', { nm_item: 'teclado', vl_preco: 150.00 });
    console.log('  status:', criado.status, '(201 = criado, diferente de 200)');
    console.log('  corpo: ', criado.corpo);
    console.log('    repare: `vl_preco` saiu como 150, e nao "150.00".');
    console.log('    quem devolveu esse corpo foi o CODIGO, com o que o cliente mandou.');
    const idCriado = JSON.parse(criado.corpo).id;
    console.log('  o id ' + idCriado + ' veio do BANCO (insertId), nao do cliente');

    console.log('');
    console.log('--- POST /item de novo, mesmo nome: o que devolve 409 ---');
    const repetido = await pedir('POST', '/item', { nm_item: 'teclado', vl_preco: 99.00 });
    console.log('  status:', repetido.status, '(409 = conflito com o que ja existe)');
    console.log('  corpo: ', repetido.corpo);

    console.log('');
    console.log('--- POST sem campo obrigatorio: o que devolve 400 ---');
    const incompleto = await pedir('POST', '/item', { nm_item: 'so o nome' });
    console.log('  status:', incompleto.status, '(400 = o pedido esta errado)');
    console.log('  corpo: ', incompleto.corpo);

    console.log('');
    console.log('--- PUT /item/' + idCriado + ': o que devolve 200 ---');
    const alterado = await pedir('PUT', '/item/' + idCriado, { nm_item: 'teclado mecanico', vl_preco: 189.00 });
    console.log('  status:', alterado.status, '(200 = alterado, nao criado)');
    console.log('  corpo: ', alterado.corpo, '  <- lido do banco, nao devolvido');
    console.log('    repare: aqui `vl_preco` saiu como "189.00", texto, e nao 189.');
    console.log('    a coluna e DECIMAL, e o DECIMAL volta string para nao perder');
    console.log('    precisao. O corpo do POST veio do objeto do cliente, e por isso');
    console.log('    saiu numero. Mesma rota de escrita, dois formatos de resposta.');

    console.log('');
    console.log('--- PUT num id que nao existe: o que devolve 404 ---');
    const inexistente = await pedir('PUT', '/item/9999', { nm_item: 'fantasma', vl_preco: 1.00 });
    console.log('  status:', inexistente.status, '(404 = o id nao esta no banco)');
    console.log('  corpo: ', inexistente.corpo);

    console.log('');
    console.log('--- DELETE /item/' + idCriado + ': o que devolve 204 ---');
    const apagado = await pedir('DELETE', '/item/' + idCriado);
    console.log('  status:', apagado.status, '(204 = apagado)');
    console.log('  corpo: ' + JSON.stringify(apagado.corpo) + '  <- VAZIO, e assim que um 204 e');
    console.log('  um 204 tem que ser: sem corpo. Mandar JSON com 204 quebra o cliente.');

    console.log('');
    console.log('--- DELETE no mesmo id outra vez: o que devolve 404 ---');
    const apagadoDeNovo = await pedir('DELETE', '/item/' + idCriado);
    console.log('  status:', apagadoDeNovo.status, '(o registro ja nao existe)');

    console.log('');
    console.log('--- o estado da tabela depois de tudo ---');
    const [restante] = await conexao.query('SELECT COUNT(*) AS n FROM tb_d15a2_item');
    console.log('  itens na tabela:', restante[0].n, '(0 = o DELETE realmente apagou)');
    console.log('');
    console.log('Os cinco status medidos, e o que cada um quer dizer:');
    console.log('  200  leu ou alterou, e ha resposta');
    console.log('  201  criou, e a criacao tem id novo');
    console.log('  204  apagou, e nao ha o que responder');
    console.log('  400  o pedido do cliente esta errado');
    console.log('  404  o recurso nao existe no banco');
    console.log('  409  o recurso ja existe com esse nome');
  } finally {
    await new Promise((resolve) => servidor.close(resolve));
    await conexao.query('DROP TABLE IF EXISTS tb_d15a2_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:40313

--- POST /item: o que devolve 201 ---
  status: 201 (201 = criado, diferente de 200)
  corpo:  {"id":1,"nm_item":"teclado","vl_preco":150}
    repare: `vl_preco` saiu como 150, e nao "150.00".
    quem devolveu esse corpo foi o CODIGO, com o que o cliente mandou.
  o id 1 veio do BANCO (insertId), nao do cliente

--- POST /item de novo, mesmo nome: o que devolve 409 ---
  POST com nome repetido: recusado com 409
  status: 409 (409 = conflito com o que ja existe)
  corpo:  {"erro":"ja existe um item com esse nome"}

--- POST sem campo obrigatorio: o que devolve 400 ---
  POST sem nm_item ou vl_preco: recusado com 400
  status: 400 (400 = o pedido esta errado)
  corpo:  {"erro":"nm_item e vl_preco sao obrigatorios"}

--- PUT /item/1: o que devolve 200 ---
  status: 200 (200 = alterado, nao criado)
  corpo:  {"id":1,"nm_item":"teclado mecanico","vl_preco":"189.00"}   <- lido do banco, nao devolvido
    repare: aqui `vl_preco` saiu como "189.00", texto, e nao 189.
    a coluna e DECIMAL, e o DECIMAL volta string para nao perder
    precisao. O corpo do POST veio do objeto do cliente, e por isso
    saiu numero. Mesma rota de escrita, dois formatos de resposta.

--- PUT num id que nao existe: o que devolve 404 ---
  PUT no id 9999: nao existe no banco, 404
  status: 404 (404 = o id nao esta no banco)
  corpo:  {"erro":"item nao encontrado"}

--- DELETE /item/1: o que devolve 204 ---
  status: 204 (204 = apagado)
  corpo: ""  <- VAZIO, e assim que um 204 e
  um 204 tem que ser: sem corpo. Mandar JSON com 204 quebra o cliente.

--- DELETE no mesmo id outra vez: o que devolve 404 ---
  DELETE no id 1: nao existe no banco, 404
  status: 404 (o registro ja nao existe)

--- o estado da tabela depois de tudo ---
  itens na tabela: 0 (0 = o DELETE realmente apagou)

Os cinco status medidos, e o que cada um quer dizer:
  200  leu ou alterou, e ha resposta
  201  criou, e a criacao tem id novo
  204  apagou, e nao ha o que responder
  400  o pedido do cliente esta errado
  404  o recurso nao existe no banco
  409  o recurso ja existe com esse nome

servidor encerrado com close(); tabela de teste removida.