Dia 15 — CRUD completo com HTTP e MySQL
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));
| passo | por quê |
|---|---|
listen(0) | pede uma porta livre ao sistema, nunca fixa |
esperar o listening | o port só existe depois que o servidor subiu |
close() no finally | sem 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.
| rota | método | o que faz |
|---|---|---|
/aluno | GET com banco | listar do banco, sem filtro |
/aluno?turma=3 | GET com banco | listar filtrado, pelo WHERE |
/aluno/7 | GET por id | buscar 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().
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ção | método | status | o que devolve |
|---|---|---|---|
POST com banco | POST | 201 | o registro criado, com o id novo |
| alterar registro | PUT | 200 | o registro relido depois da alteração |
| alterar registro | PATCH | 200 | só o que mudou, e nem toda tabela aceita |
DELETE com banco | DELETE | 204 | nada — 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:
| rota | o corpo vem de | vl_preco sai como |
|---|---|---|
POST | do objeto que o cliente mandou | 150 (number) |
PUT | de 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
| status | quando |
|---|---|
200 | leu ou alterou, e há resposta |
201 | criou, e a criação tem id novo |
204 | apagou, e não há o que responder |
400 | o pedido do cliente está errado |
404 | o recurso não existe no banco |
409 | o 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.