Dia 1 — Onde o 2º trimestre parou
Revisão da API do 2º trimestre
O que a API do 2º trimestre faz
O trimestre passado terminou com um CRUD completo: quatro rotas, um arquivo, um MySQL. As quatro operações funcionam, e é por isso que elas servem de ponto de partida — o código do exemplo abaixo é o mesmo que estava rodando.
| Rota | O que faz | Status |
|---|---|---|
GET /produtos | lista todos | 200 |
GET /produtos/1 | busca por id | 200 ou 404 |
POST /produtos | cria | 201 com o id novo |
DELETE /produtos/1 | remove | 200 com removidos |
Consultas parametrizadas, await, try/catch, servidor que sobe e desce: nada disso faltou. O que faltou são seis coisas, e o exemplo do fim mede duas delas rodando de verdade contra o servidor no ar.
Esta é a revisão de abertura: a API do trimestre passado é medida antes de ser mudada, e o que fica é a lista do que ela faz ao lado do que ela não decide. Cada rota por operação aparece com a consulta que a serve, e o código que cresceu aparece inteiro — quatro rotas, quatro blocos de SQL e um try/catch único que trata erro de negócio e erro técnico no mesmo lugar. Revisar o próprio código com o exemplo rodando é o que separa este dia de uma mudança às cegas: o defeito não é o que o código faz, é o que ele não tem decidido. O que ficou do 2º trimestre são exatamente essas duas coisas, e cada bloco de três dias adiante pega uma delas.
Onde a dívida técnica aparece
Dívida técnica é o custo que se paga por uma decisão tomada com pressa. No CRUD ela não aparece como código quebrado — aparece como decisões que impedem a próxima mudança.
A camada sumiu com a rota. Rota, consulta e resposta estão no mesmo bloco. Isso funcionou com quatro rotas. Na quarta alteração, quem precisar mudar a consulta vai abrir o mesmo arquivo onde está a resposta HTTP, e o try/catch do servidor vai engolir o erro técnico como se fosse erro de negócio.
Não existe dono da requisição. O DELETE abaixo vai sem token, sem header, sem nada — e remove. Não é um bug de implementação: é a ausência de uma decisão. Enquanto ninguém decidir quem pode remover, qualquer pessoa na internet remove.
A validação não existe. O POST com nm_item vazio e qtd zero responde 201 e grava. A tabela aceitou o que a API não conferiu, e a contagem do exemplo sai da consulta — não de um palpite.
O esquema não tem dono. As tabelas nascem de CREATE TABLE dentro do arquivo que por acaso roda primeiro. Trocar de arquivo pode mudar o esquema sem querer, e o erro aparece na consulta que roda depois.
Não há teste. A pasta não existe. Uma correção no dia 10 pode desfazer a do dia 3, e ninguém percebe sem uma requisição na mão.
Não há registro do que rodou. O NODE_ENV está vazio: a API não sabe se está em máquina de quem estuda ou em produção, e a porta é escolhida em tempo de execução por um listen(0).
O que a medição mostra
O bloco do fim do exemplo não comenta os defeitos — ele provoca dois deles e conta.
O DELETE vai sem credencial nenhuma e devolve 200 com removidos: 1. O número sai do affectedRows que o driver devolveu. Ninguém perguntou quem fez a requisição.
O POST manda nm_item: '' com qtd: 0 e recebe 201. Em seguida vem um SELECT COUNT(*) com os mesmos filtros, e a contagem sai 1: a linha inválida está no banco.
Duas medidas, dois defeitos de arquitetura confirmados no mesmo processo que serviu as requisições. O TRUNCATE no começo e a limpeza no fim existem para o exemplo rodar duas vezes dando o mesmo resultado.
O que o trimestre resolve
Cada bloco de três dias do trimestre pega uma promessa que ficou no 2º e que a API não cumpriu:
| Bloco | Promessa pendente |
|---|---|
| dias 1 e 2 | organizar o código em camadas |
| dias 3 e 4 | autenticação e rota protegida |
| dias 5 e 6 | teste e validação de entrada |
| dias 7 e 8 | migração de banco |
| dias 9 e 10 | segurança e segredo fora do código |
| dias 11 e 12 | medir a consulta antes de otimizar |
| dias 13 e 14 | concorrência e transação |
| dias 15 e 16 | regra de negócio e estado |
| dias 17 e 18 | trabalho que demora |
| dias 19 e 20 | log e rastreio |
| dias 21 e 22 | processo e ambiente |
| dias 23 e 24 | documentação e entrega |
| dias 25 e 26 | fechamento do ano |
O assunto não muda: continua sendo Node.js e MySQL. O que muda é a exigência — o código precisa sobreviver a outra pessoa, a uma mudança de esquema e a um restart.
O
status: 200doDELETEsem token é a medida mais importante do exemplo. Um código que "funciona" na tela não prova que está protegido: prova que responde. O que protege é a decisão de quem pode chamar, e decisão se escreve antes de escrever a rota.
O exemplo declara a própria conexão com
require('mysql2/promise')e fecha comend(). Uma conexão compartilhada que se fecha sozinha quando não há consulta em voo não sobrevive a um servidor HTTP mantendo requisições entre uma consulta e outra — e o aluno que copiar o exemplo para um projeto real precisa ver orequiree oend().
Exemplo
'use strict'; // Exemplo da aula 1 do dia 1: a API do 2º trimestre funcionando de verdade, // e a medicao do que ela deixou de fazer. // // A API e a do trimestre passado — o CRUD inteiro em um arquivo so. Ela // funciona: as quatro rotas respondem e o dado vai para o MySQL. E e // exatamente por funcionar que ela serve aqui: o trimestre comeca olhando // codigo que roda, nao codigo imaginado. // // O `diagnostico` do fim nao e opiniao. Cada linha sai de uma medicao real: // uma requisicao feita sem credencial, um INSERT que aceitou dado invalido, // uma pasta procurada no disco. // // POR QUE O EXEMPLO ABRE A CONEXAO PROPRIA // O `conexao` que o material entrega e do harness, e ele existe para exemplos // curtos. Um exemplo que segura a conexao enquanto um servidor HTTP atende // requisicoes nao pode usar a compartilhada: entre uma consulta e outra passa // tempo sem nenhuma em voo, e a conexao e fechada por baixo. Por isso o // exemplo declara o proprio `require('mysql2')` e fecha com `end()` no // `finally` — que e o que o codigo de producao faz tambem. const http = require('node:http'); const fs = require('node:fs'); const { createConnection } = require('mysql2/promise'); async function main() { const c = await createConnection({ host: process.env.DB_HOST, port: Number(process.env.DB_PORT), user: process.env.DB_USER, password: process.env.DB_PASS, database: process.env.DB_NAME, multipleStatements: true, }); // -------------------------------------------------------------- esquema await c.query(` CREATE TABLE IF NOT EXISTS tb_revisao ( id INT AUTO_INCREMENT PRIMARY KEY, nm_item VARCHAR(40) NOT NULL, qtd INT NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 `); // `TRUNCATE` e nao `DELETE` porque ele tambem zera o `AUTO_INCREMENT`. Com // `DELETE` o id da proxima insercao continuaria crescendo a cada execucao, // e a saida embutida na pagina mudaria entre uma rodada e outra. Aqui o // exemplo comeca sempre do mesmo estado. await c.query('TRUNCATE TABLE tb_revisao'); await c.query('INSERT INTO tb_revisao (id, nm_item, qtd) VALUES (?, ?, ?)', [1, 'teclado', 2]); // --------------------------------------------------------------- a API // Rota, consulta e resposta no mesmo lugar: e o estado em que o 2º // trimestre terminou. async function json(res, status, corpo) { res.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8' }); res.end(JSON.stringify(corpo)); } async function lerCorpo(req) { const partes = []; for await (const p of req) partes.push(p); const bruto = Buffer.concat(partes).toString('utf8'); return bruto ? JSON.parse(bruto) : {}; } const rotas = { 'GET /produtos': async (_req, res) => { const [linhas] = await c.query('SELECT * FROM tb_revisao ORDER BY id'); await json(res, 200, { dados: linhas }); }, 'GET /produtos/1': async (_req, res) => { const [linhas] = await c.query('SELECT * FROM tb_revisao WHERE id = ?', [1]); await json(res, linhas.length ? 200 : 404, linhas.length ? linhas[0] : { erro: 'item nao encontrado' }); }, 'POST /produtos': async (req, res) => { const novo = await lerCorpo(req); const [r] = await c.execute( 'INSERT INTO tb_revisao (nm_item, qtd) VALUES (?, ?)', [novo.nm_item, novo.qtd]); await json(res, 201, { id: r.insertId }); }, 'DELETE /produtos/1': async (_req, res) => { const [r] = await c.execute('DELETE FROM tb_revisao WHERE id = ?', [1]); await json(res, 200, { removidos: r.affectedRows }); }, }; const servidor = http.createServer(async (req, res) => { const rota = rotas[`${req.method} ${req.url}`]; if (!rota) { await json(res, 404, { erro: 'rota nao encontrada' }); return; } await rota(req, 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 ao // sistema, e por isso o numero muda a cada execucao. await new Promise((r) => servidor.listen(0, '127.0.0.1', r)); const base = 'http://127.0.0.1:' + servidor.address().port; console.log('API do 2º trimestre no ar em ' + base); console.log('a porta muda a cada execucao: e o listen(0) pedindo uma livre'); const pedir = (metodo, caminho, corpo) => fetch(base + caminho, { method: metodo, headers: corpo ? { 'Content-Type': 'application/json' } : undefined, body: corpo ? JSON.stringify(corpo) : undefined, }); try { console.log('\n--- as quatro rotas do CRUD ---'); let r = await pedir('GET', '/produtos'); console.log('GET /produtos -> ' + r.status + ' ' + await r.text()); r = await pedir('GET', '/produtos/1'); console.log('GET /produtos/1 -> ' + r.status + ' ' + await r.text()); r = await pedir('POST', '/produtos', { nm_item: 'mouse', qtd: 5 }); console.log('POST /produtos -> ' + r.status + ' ' + await r.text()); // -------------------------------------------------------------- medico // As duas medicoes que o trimestre resolve. Nenhuma delas epalpite: sao // requisicoes de verdade contra o servidor que esta no ar agora. console.log('\n--- o que a API aceita hoje ---'); // Medida 1: nenhuma credencial e pedida. O DELETE abaixo vai sem token, // sem header de autenticacao, sem nada. E mesmo assim ele remove. r = await pedir('DELETE', '/produtos/1'); console.log('DELETE sem token algum -> ' + r.status + ' ' + await r.text()); // Medida 2: nenhum campo e conferido. O item abaixo esta vazio e com // quantidade zero, e mesmo assim a rota responde 201. r = await pedir('POST', '/produtos', { nm_item: '', qtd: 0 }); console.log('POST com dado invalido -> ' + r.status + ' ' + await r.text()); // `query` do mysql2 devolve `[linhas, campos]`: o array de linhas vem no // primeiro elemento. E por isso que o desmonte e `[conta]`, e nao `conta` // — que seria o array inteiro, e nao a linha. const [conta] = await c.query( 'SELECT COUNT(*) AS n FROM tb_revisao WHERE nm_item = ? OR qtd = ?', ['', 0]); console.log('linhas invalidas gravadas no banco: ' + conta[0].n + ' — a tabela aceitou o que a API nao conferiu'); // Limpa o dado invalido para o exemplo nao deixar estado para tras. await c.query('DELETE FROM tb_revisao WHERE nm_item = ? OR qtd = ?', ['', 0]); } finally { await new Promise((r) => servidor.close(r)); console.log('\nservidor encerrado com close().'); } // ------------------------------------------------------------- estrutura // O que o repositorio tem e o que nao tem. `existsSync` procura de verdade. console.log('--- estrutura do projeto ---'); const perto = (nome) => fs.existsSync(__dirname + '/../' + nome); for (const [nome, papel] of [ ['teste', 'pasta de teste'], ['migracoes', 'pasta de migracoes'], ['src', 'pasta src por camada'], ]) { console.log(papel + ': ' + (perto(nome) ? 'existe' : 'nao existe')); } const [tabelas] = await c.query('SHOW TABLES LIKE ?', ['%migrac%']); console.log('tabela de migracoes no banco: ' + (tabelas.length ? tabelas.length + ' encontrada(s)' : 'nenhuma')); const [versao] = await c.query('SELECT VERSION() AS v'); console.log('esquema criado por este arquivo, rodando em ' + versao[0].v); console.log('NODE_ENV: ' + (process.env.NODE_ENV || '(vazio — a API nao sabe ' + 'em que ambiente esta)')); await c.end(); } main().catch((erro) => { console.error('falhou:', erro.code || erro.name, '-', erro.message); process.exit(1); });
Saída real
API do 2º trimestre no ar em http://127.0.0.1:36239
a porta muda a cada execucao: e o listen(0) pedindo uma livre
--- as quatro rotas do CRUD ---
GET /produtos -> 200 {"dados":[{"id":1,"nm_item":"teclado","qtd":2}]}
GET /produtos/1 -> 200 {"id":1,"nm_item":"teclado","qtd":2}
POST /produtos -> 201 {"id":2}
--- o que a API aceita hoje ---
DELETE sem token algum -> 200 {"removidos":1}
POST com dado invalido -> 201 {"id":3}
linhas invalidas gravadas no banco: 1 — a tabela aceitou o que a API nao conferiu
servidor encerrado com close().
--- estrutura do projeto ---
pasta de teste: nao existe
pasta de migracoes: nao existe
pasta src por camada: nao existe
tabela de migracoes no banco: 1 encontrada(s)
esquema criado por este arquivo, rodando em 10.11.14-MariaDB-0ubuntu0.24.04.1
NODE_ENV: (vazio — a API nao sabe em que ambiente esta)
Separar em camadas: rota, serviço, repositório
Por que separar em camadas
O CRUD do 2º trimestre tinha rota, consulta e resposta no mesmo bloco. Isso funcionou com quatro rotas e quebra na quinta mudança. Separar responsabilidades não é arrumação de arquivos: é fazer com que cada decisão tenha um dono, e só um.
O critério de uma boa separação é o que o exemplo do dia mede: trocar uma camada sem tocar nas outras. No fim do exemplo o repositório é substituído por um que não fala com banco nenhum, e a mesma rota e o mesmo serviço continuam funcionando.
As três camadas
| Camada | Sabe de | Não sabe de |
|---|---|---|
| rota | HTTP, status, JSON | SQL, regra |
| serviço | regra de negócio | SQL, HTTP |
| repositório | SQL, tabela, coluna | HTTP, regra |
O fluxo é sempre o mesmo, e ele é o que dá nome às camadas:
requisição → rota → serviço → repositório → MySQL ↑ ↓ └──── status ← erro ───┘
Cada camada tem nome em inglês no código de mercado, e o mesmo papel aparece com nomes diferentes: a que traduz HTTP é o controller, ou route handler quando a função só faz o roteamento; a que decide é o service; a que fala com o banco é o repository. Os três termos aparecem em código de framework, e a arquitetura em camadas é a mesma em qualquer um deles.
A rota traduz. Lê req.method e req.url, chama um método do serviço, e converte o retorno em res.writeHead com JSON. Ela não sabe por que qtd negativa é erro — sabe apenas que o serviço lançou algo com status.
const responder = (status, corpo) => { res.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8' }); res.end(JSON.stringify(corpo)); };
O serviço decide. Não sabe que existe tabela, coluna ou SELECT. Recebe objeto, devolve objeto. A regra inteira mora aqui: o que é obrigatório, o que é repetido, o que é conflito.
if (!nmItem) throw new ErroDeNegocio('nm_item e obrigatorio', 400); if (!Number.isInteger(qtd) || qtd <= 0) { throw new ErroDeNegocio('qtd deve ser inteiro maior que zero', 400); }
O repositório executa. É a única camada com SQL. Também é onde mora o filtro por nome: o serviço pergunta "existe um item com esse nome?" e não sabe que a resposta sai de WHERE nm_item = ?.
Erro de negócio e erro técnico
A divisão que a aula do dia 11 detalha nasce aqui, e é a razão de o serviço lançar um tipo próprio em vez de throw new Error.
class ErroDeNegocio extends Error { constructor(mensagem, status) { super(mensagem); this.name = 'ErroDeNegocio'; this.status = status; } }
A rota distingue os dois com uma única pergunta:
if (erro instanceof ErroDeNegocio) { return responder(erro.status, { erro: erro.message }); } responder(500, { erro: 'falha interna' });
O message do erro de negócio vai para o cliente, porque ele foi escrito para ser lido por quem está usando a API. O detalhe do erro técnico não vai: o console.error guarda, a resposta devolve cinco palavras.
No exemplo, os quatro status saem da consulta ao banco e da decisão do serviço, um por um: 201 no item válido, 409 no nome repetido, 400 no nome vazio, 400 na quantidade negativa, 404 no id que não existe.
Onde a lógica entra
A pergunta que a divisão responde é sempre a mesma: onde a lógica entra. A resposta tem três critérios, e a ordem importa — o que decide desce para a camada de baixo pela injeção, e o que só é tradução fica na de cima. Uma regra escrita no repositório vira SQL; uma regra escrita na rota vira if que ninguém acha; a regra escrita no serviço é a única que os dois lados conseguem ler.
No disco isso aparece como uma pasta src com um módulo por camada, e o nome do arquivo diz a camada:
src/rota/produto.js—http.createServer, status, JSONsrc/servico/produto.js— regra de negócio,ErroDeNegociosrc/repositorio/produto.js— SQL, tabelatb_item
O import entre camadas respeita a seta e só a seta: a rota importa o serviço, e o serviço recebe o repositório por parâmetro sem importá-lo. O caminho de arquivo é o que a busca usa, e ele é escrito com barra: src/repositorio/produto.js.
Quando a pasta src não existe, o sinal é o próprio arquivo: se a rota e o SELECT estão no mesmo .js, ainda não há camadas — há um arquivo que sabe tudo.
Injeção de dependência
As camadas se ligam por parâmetro, não por require de dentro. criarServico(repo) recebe o repositório pronto; criarRota(servico) recebe o serviço pronto. Nenhuma das duas funções importa a camada de baixo.
const repo = criarRepositorio(c); const servico = criarServico(repo); const tratar = criarRota(servico);
É essa assinatura que torna a prova do fim do exemplo possível. criarServico(repoFalso) recebe um objeto que devolve dados de memória, e todo o resto da pilha continua igual — inclusive a mensagem de 409, que saiu da regra e não do banco.
O c do repositório vem de createConnection e fecha com end(). Uma conexão compartilhada não sobrevive a requisições que passam tempo sem consulta em voo, e o aluno que copiar o exemplo precisa ver o require e o end() na tela.
O que a troca de repositório prova
O segundo bloco do exemplo sobe outro servidor, com a mesma criarRota e o mesmo criarServico, e um repositório que devolve { id: 1, nm_item: 'memoria', qtd: 99 } sem tocar em banco. O GET /itens responde 200 com esse objeto, e o POST com nome repetido responde 409 com a mesma mensagem do banco real.
Duas respostas iguais vindas de lugares diferentes, no mesmo processo. É isso que a camada compra: a regra de negócio passa a ser testável sem banco, e o próximo bloco do trimestre vai usar exatamente isso.
O erro mais comum ao começar a separar é empurrar a regra para o repositório porque é "mais fácil" — o serviço vira um
pass-throughque só repassa parâmetro, e a divisão não compra nada. O sinal é o serviço não ter nenhumifpróprio.
Separar em três camadas não é obrigatória em toda API. É obrigatória quando a regra existe, é testada por outra pessoa e o dado já tem dono. Uma API de leitura sem regra nenhuma fica mais clara em duas.
Exemplo
'use strict'; // Exemplo da aula 2 do dia 1: o mesmo CRUD em tres camadas. // // As tres camadas existem por uma razao que da para medir: cada uma pode ser // trocada sem tocar nas outras. O exemplo monta as tres e depois faz a prova — // troca o banco de lugar sem tocar no SQL, e troca a regra sem tocar na rota. const http = require('node:http'); const { createConnection } = require('mysql2/promise'); // ============================================================ CAMADA 3: REPOSITORIO // A unica camada que conhece SQL. Nao sabe o que e HTTP, nao sabe o que e // regra de negocio, nao sabe o que e JSON. Recebe nome e devolve linha. function criarRepositorio(c) { return { async listar() { const [linhas] = await c.query('SELECT * FROM tb_camadas ORDER BY id'); return linhas; }, async buscarPorId(id) { const [linhas] = await c.query('SELECT * FROM tb_camadas WHERE id = ?', [id]); return linhas[0] || null; }, async criar(nmItem, qtd) { const [r] = await c.execute( 'INSERT INTO tb_camadas (nm_item, qtd) VALUES (?, ?)', [nmItem, qtd]); return r.insertId; }, // O filtro por nome mora AQUI, e nao no servico: o servico diz "existe um // item com esse nome?" sem saber que existe uma coluna `nm_item`, e sem // saber que a busca e por igualdade. async buscarPorNome(nome) { const [linhas] = await c.query( 'SELECT * FROM tb_camadas WHERE nm_item = ?', [nome]); return linhas[0] || null; }, async remover(id) { const [r] = await c.execute('DELETE FROM tb_camadas WHERE id = ?', [id]); return r.affectedRows; }, }; } // ============================================================ CAMADA 2: SERVICO // A unica camada que conhece regra de negocio. Nao sabe SQL e nao sabe HTTP. // Recebe objeto, devolve objeto — ou lanca ErroDeNegocio. class ErroDeNegocio extends Error { constructor(mensagem, status) { super(mensagem); this.name = 'ErroDeNegocio'; this.status = status; } } function criarServico(repo) { return { async listar() { return repo.listar(); }, async buscarPorId(id) { const item = await repo.buscarPorId(id); if (!item) { // Regra que depende do dado: o id existe? Nao existe e um erro de // negocio (404), nao um erro tecnico (500). throw new ErroDeNegocio('item nao encontrado', 404); } return item; }, async criar(dados) { // Valida e normaliza ANTES de tocar o banco. const nmItem = String(dados.nm_item ?? '').trim(); const qtd = Number(dados.qtd); if (!nmItem) throw new ErroDeNegocio('nm_item e obrigatorio', 400); if (!Number.isInteger(qtd) || qtd <= 0) { throw new ErroDeNegocio('qtd deve ser inteiro maior que zero', 400); } const repetido = await repo.buscarPorNome(nmItem); if (repetido) { throw new ErroDeNegocio('ja existe item com esse nome', 409); } return repo.criar(nmItem, qtd); }, async remover(id) { const removidos = await repo.remover(id); if (removidos === 0) { throw new ErroDeNegocio('item nao encontrado', 404); } return removidos; }, }; } // ============================================================ CAMADA 1: ROTA // A unica camada que conhece HTTP. Nao sabe SQL e nao sabe a regra: so // traduz requisição em chamada de serviço e erro em status. function criarRota(servico) { return async function tratar(req, res) { const responder = (status, corpo) => { res.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8' }); res.end(JSON.stringify(corpo)); }; const partes = []; for await (const p of req) partes.push(p); const bruto = Buffer.concat(partes).toString('utf8'); const dados = bruto ? JSON.parse(bruto) : {}; const id = Number(req.url.split('/').pop()); try { if (req.method === 'GET' && req.url === '/itens') { return responder(200, { dados: await servico.listar() }); } if (req.method === 'GET' && Number.isInteger(id)) { return responder(200, await servico.buscarPorId(id)); } if (req.method === 'POST' && req.url === '/itens') { const idNovo = await servico.criar(dados); return responder(201, { id: idNovo }); } if (req.method === 'DELETE' && Number.isInteger(id)) { return responder(200, { removidos: await servico.remover(id) }); } return responder(404, { erro: 'rota nao encontrada' }); } catch (erro) { // A rota distingue os dois tipos de erro pelo que o servico lancou. if (erro instanceof ErroDeNegocio) { return responder(erro.status, { erro: erro.message }); } // Erro tecnico: a rota nao devolve o detalhe para o cliente. responder(500, { erro: 'falha interna' }); console.error('erro tecnico:', erro.code || erro.name, erro.message); } }; } // ============================================================ LIGACAO async function main() { const c = await createConnection({ host: process.env.DB_HOST, port: Number(process.env.DB_PORT), user: process.env.DB_USER, password: process.env.DB_PASS, database: process.env.DB_NAME, multipleStatements: true, }); await c.query(` CREATE TABLE IF NOT EXISTS tb_camadas ( id INT AUTO_INCREMENT PRIMARY KEY, nm_item VARCHAR(40) NOT NULL, qtd INT NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 `); await c.query('TRUNCATE TABLE tb_camadas'); // O repositorio real, com SQL de verdade. const repo = criarRepositorio(c); const servico = criarServico(repo); const tratar = criarRota(servico); const servidor = http.createServer(tratar); await new Promise((r) => servidor.listen(0, '127.0.0.1', r)); const base = 'http://127.0.0.1:' + servidor.address().port; console.log('mesmo CRUD, agora em tres camadas, em ' + base); console.log('a porta muda a cada execucao: e o listen(0) pedindo uma livre'); const pedir = (metodo, caminho, corpo) => fetch(base + caminho, { method: metodo, headers: corpo ? { 'Content-Type': 'application/json' } : undefined, body: corpo ? JSON.stringify(corpo) : undefined, }); try { console.log('\n--- rota servindo, servico decidindo, repositorio consultando ---'); let r = await pedir('POST', '/itens', { nm_item: 'teclado', qtd: 2 }); console.log('POST valido -> ' + r.status + ' ' + await r.text()); r = await pedir('POST', '/itens', { nm_item: 'teclado', qtd: 3 }); console.log('POST nome repetido-> ' + r.status + ' ' + await r.text()); r = await pedir('POST', '/itens', { nm_item: ' ', qtd: 1 }); console.log('POST nome vazio -> ' + r.status + ' ' + await r.text()); r = await pedir('POST', '/itens', { nm_item: 'mouse', qtd: -5 }); console.log('POST qtd negativa -> ' + r.status + ' ' + await r.text()); r = await pedir('GET', '/itens'); console.log('GET lista -> ' + r.status + ' ' + await r.text()); r = await pedir('GET', '/99'); console.log('GET id inexistente-> ' + r.status + ' ' + await r.text()); } finally { await new Promise((r) => servidor.close(r)); console.log('\nservidor encerrado com close().'); } // ------------------------------------------------------- a prova das camadas // Um repositorio que nao e o MySQL: o mesmo servico e a mesma rota, sem // tocar em uma linha do SQL acima. E isso que "camada" significa. const [tabelas] = await c.query('SHOW TABLES LIKE ?', ['%falso%']); console.log('tabela no banco para o repositorio falso: ' + (tabelas.length ? 'existe' : 'nenhuma — ele nem fala com o banco')); const repoFalso = { async listar() { return [{ id: 1, nm_item: 'memoria', qtd: 99 }]; }, async buscarPorId(id) { return id === 1 ? { id: 1, nm_item: 'memoria', qtd: 99 } : null; }, async buscarPorNome(nome) { return nome === 'teclado' ? { id: 1, nm_item: 'teclado', qtd: 2 } : null; }, async criar() { return 500; }, async remover() { return 0; }, }; const servicoFalso = criarServico(repoFalso); const tratarFalso = criarRota(servicoFalso); const servidor2 = http.createServer(tratarFalso); await new Promise((r) => servidor2.listen(0, '127.0.0.1', r)); const base2 = 'http://127.0.0.1:' + servidor2.address().port; console.log('\nmesma rota e mesmo servico, repositorio trocado (em ' + base2 + ')'); try { let r2 = await fetch(base2 + '/itens'); console.log('GET lista no falso-> ' + r2.status + ' ' + await r2.text()); r2 = await fetch(base2 + '/itens', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ nm_item: 'teclado', qtd: 1 }), }); console.log('POST repetido -> ' + r2.status + ' ' + await r2.text() + ' (regra do servico, sem tocar no banco)'); } finally { await new Promise((r) => servidor2.close(r)); console.log('segundo servidor encerrado com close().'); } console.log('\na regra do servico rodou duas vezes, em dois bancos diferentes'); await c.end(); } main().catch((erro) => { console.error('falhou:', erro.code || erro.name, '-', erro.message); process.exit(1); });
Saída real
mesmo CRUD, agora em tres camadas, em http://127.0.0.1:33919
a porta muda a cada execucao: e o listen(0) pedindo uma livre
--- rota servindo, servico decidindo, repositorio consultando ---
POST valido -> 201 {"id":1}
POST nome repetido-> 409 {"erro":"ja existe item com esse nome"}
POST nome vazio -> 400 {"erro":"nm_item e obrigatorio"}
POST qtd negativa -> 400 {"erro":"qtd deve ser inteiro maior que zero"}
GET lista -> 200 {"dados":[{"id":1,"nm_item":"teclado","qtd":2}]}
GET id inexistente-> 404 {"erro":"item nao encontrado"}
servidor encerrado com close().
tabela no banco para o repositorio falso: nenhuma — ele nem fala com o banco
mesma rota e mesmo servico, repositorio trocado (em http://127.0.0.1:34875)
GET lista no falso-> 200 {"dados":[{"id":1,"nm_item":"memoria","qtd":99}]}
POST repetido -> 409 {"erro":"ja existe item com esse nome"} (regra do servico, sem tocar no banco)
segundo servidor encerrado com close().
a regra do servico rodou duas vezes, em dois bancos diferentes