Nível 2Unidade 3 · Web dinâmica server-side3 aulas de 50 min + 1 h EAD
Aula 12 — Express estruturado e middlewares
Nível 2 — Desenvolvimento Web · WebLab
O server.js da Aula 11 funciona: serve o site, devolve o cardápio em JSON e responde 404 para um id inexistente. Mas ele é um arquivo só, e um arquivo só não escala. Imagine quarenta rotas, validações, regras de negócio e log de acesso empilhados ali dentro. Hoje aplicamos ao servidor exatamente o princípio que você já usa no front desde a Unidade 1 — separação de responsabilidades —, só que entre arquivos. E, para isso, você precisa conhecer a peça que sustenta o Express inteiro: o middleware.
Na aula passada você criou o repositório cafe-cerrado-api, subiu um servidor Express 5, serviu o site pela pasta public/ e escreveu os endpoints GET /api/produtos, GET /api/produtos/:id e GET /api/categorias. Tudo isso mora em um único server.js. Hoje esse arquivo é quebrado em peças, ganha a capacidade de receber dados (não só devolver) e aprende a falhar de forma organizada. Os três endpoints continuam existindo do começo ao fim — eles apenas mudam de casa, cada recurso no seu Router.
Antes de começar, confirme:
[ ] cafe-cerrado-api clonado ou aberto, com npm install já rodado.
[ ] npm run dev sobe o servidor e http://localhost:3000 mostra o site.
[ ] curl -i http://localhost:3000/api/produtos devolve 200 e o array de produtos.
[ ] Extensão REST Client instalada no VS Code (humao.rest-client).
[ ] data/produtos.json com pelo menos seis itens, cada um com id, nome, categoria, preco, descricao e imagem.
Um middleware é uma função que recebe (req, res, next) e roda entre a chegada da requisição e a resposta final. Ela pode fazer três coisas:
Examinar ou modificarreq e res (por exemplo, acrescentar req.usuario depois de conferir um token).
Responder e encerrar a requisição ali mesmo (res.json(...)), sem chamar next().
Passar adiante, chamando next(), para que o próximo middleware da fila decida o que fazer.
A analogia: uma linha de montagem com estações de inspeção. A peça (a requisição) entra por um lado e passa estação por estação. Cada estação pode carimbar a peça, rejeitá-la ali mesmo ou deixá-la seguir. Uma rota — app.get("/api/produtos", ...) — é só a última estação da linha, a que costuma produzir a resposta.
JavaScript
// Middleware de log: roda para TODA requisição que chega ao servidor.app.use((req,res,next)=>{console.log(`${newDate().toISOString()}${req.method}${req.originalUrl}`);next();// sem next(), a requisição "trava" aqui e nunca chega às rotas});
Middlewares são registrados com app.use(...) (ou com app.get, app.post, etc.) e executados na ordem em que aparecem no arquivo. Não é uma sugestão: é o mecanismo. Trocar duas linhas de lugar muda o comportamento do servidor.
JavaScript
app.use(express.json());// 1º: transforma o corpo JSON em objetoapp.use(registrarRequisicao);// 2º: escreve no console o que chegouapp.use(express.static("public"));// 3º: tenta achar um arquivo em public/app.use("/api/produtos",produtosRouter);// 4º: tenta casar com uma rota de produtoapp.use("/api",naoEncontradoApi);// 5º: só chega aqui quem não casou acimaapp.use(tratadorDeErros);// 6º: só roda quando algo lançou erro
Uma requisição para GET /css/estilo.css percorre 1, 2, 3 — e para no 3, porque o arquivo existe. Uma requisição para GET /api/produtos percorre 1, 2, 3 (não achou arquivo), e para no 4. Uma requisição para GET /api/pedidos percorre 1, 2, 3, 4 (nenhuma rota de produto casou) e para no 5.
express.static("public") é um middleware: ele procura o arquivo pedido; se acha, responde e encerra; se não acha, chama next() e a requisição segue.
As próprias rotas são middlewares — só que com uma condição extra: só rodam se o método e o caminho casarem.
Ou seja, app.get("/api/produtos", handler) é açúcar sintático para "registre este middleware, mas só execute quando o método for GET e o caminho for /api/produtos".
// BUG: este middleware nunca deixa a requisição seguir.app.use((req,res,next)=>{console.log("Chegou:",req.originalUrl);// faltou o next() aqui});
O servidor não quebra. Não há erro no terminal. Simplesmente nada acontece: o navegador fica com a abinha girando até desistir, e na aba Network o status da requisição é (pending) — pendente — para sempre. É um bug silencioso e assustador na primeira vez.
A regra que resolve: todo middleware precisa terminar chamando next() ou enviando uma resposta. Nunca as duas coisas, nunca nenhuma das duas.
🔬 Investigue
No seu server.js, acrescente logo no começo um middleware de log e não chame next(). Rode npm run dev, abra http://localhost:3000 e observe: a aba Network do DevTools mostra a requisição em (pending); o terminal mostra a linha de log. Agora abra outra aba e peça o CSS: a requisição também fica pendurada, mas o log aparece — prova de que o middleware rodou e a cadeia parou nele. Meça quanto tempo o Chrome espera antes de desistir. Depois acrescente o next() e confirme que tudo volta ao normal. Anote a diferença entre "servidor travado" e "requisição travada": o processo Node continua atendendo outras conexões o tempo todo.
Na Aula 10 o seu front-end enviou JSON.stringify(dados) no corpo de um POST. Do lado do servidor, esse corpo chega como uma sequência de bytes crua — o Express não a interpreta sozinho, por decisão de projeto (nem toda API recebe JSON). É preciso ligar o parser:
JavaScript
app.use(express.json());// SEM esta linha, req.body é undefined
Com ele registrado, o Express olha o cabeçalho Content-Type da requisição; se for application/json, lê o corpo, converte com JSON.parse e deixa o resultado em req.body.
JavaScript
app.post("/api/produtos",(req,res)=>{const{nome,preco}=req.body;// o objeto enviado pelo fetchif(!nome||typeofpreco!=="number"){returnres.status(400).json({erro:"Os campos nome e preco são obrigatórios."});}constnovo={id:proximoId++,nome,preco};produtos.push(novo);res.status(201).json(novo);// 201 Created + o recurso criado});
⚠️ Atenção
No Express 5, req.body é undefined quando nenhum parser rodou — e não um objeto vazio. Destruturar undefined derruba a requisição com TypeError: Cannot destructure property 'nome' of 'req.body' as it is undefined. Se a mensagem aparecer, a causa é quase sempre uma destas três: falta app.use(express.json()); o parser foi registrado depois da rota; ou o cliente esqueceu o cabeçalho Content-Type: application/json.
💡 Dica
Tutoriais de Express 4 mandam rodar npm install body-parser e usar bodyParser.json(). Não instale nada: desde o Express 4.16 os parsers express.json() e express.urlencoded() são nativos, e no Express 5 continuam sendo. Uma dependência a menos.
2.2 express.urlencoded(): o formulário HTML clássico¶
Um <form method="post"> sem JavaScript não envia JSON — envia application/x-www-form-urlencoded, o formato nome=Cafe&preco=7.5. Para lê-lo:
JavaScript
app.use(express.urlencoded({extended:true}));
Os dois parsers podem conviver: cada um olha o Content-Type e só age no formato que entende. O Café Cerrado usa JSON (o front envia por fetch), mas registrar os dois é barato e evita surpresa quando alguém testa com um formulário puro.
Lembre das camadas: na Aula 03 o HTML validou com required e type="email"; na Aula 07 o JavaScript validou com mensagens por campo. E, mesmo assim, o servidor valida de novo. Por quê?
Porque qualquer pessoa pode mandar um POST direto para a sua API — com curl, com o REST Client, com um script de dez linhas — pulando o seu front-end inteiro. As validações do navegador servem para dar feedback rápido e gentil a quem está de boa-fé. A única validação em que se pode confiar é a que roda no servidor, porque é a única que o cliente não controla.
E validação de servidor não é opcional nem "nível avançado": é o mínimo. Sem ela, um POST com {"preco": "de graça"} entra na sua base e quebra a página de todo mundo na próxima renderização.
Sucesso em consulta ou alteração, com corpo na resposta
201 Created
Recurso criado com sucesso — resposta típica do POST
204 No Content
Sucesso sem corpo — resposta típica do DELETE
400 Bad Request
Os dados enviados pelo cliente são inválidos
404 Not Found
O recurso pedido, ou a própria rota, não existe
500 Internal Server Error
Algo quebrou dentro do seu servidor
401 (não autenticado) e 403 (autenticado, mas sem permissão) entram na Aula 14, quando o login Google chegar.
📌 Vale gravar
A diferença entre 400 e 500 é de culpa. 4xx diz "o problema está no que você mandou"; 5xx diz "o problema está aqui dentro, desculpe". Devolver 500 para um campo faltando é mentir para o cliente: ele vai ficar tentando de novo, achando que é instabilidade do servidor, quando bastava corrigir o corpo da requisição.
🧠 Você sabia?
O Express foi criado em 2010 por TJ Holowaychuk, inspirado no Sinatra, um microframework de Ruby. A versão 4.0 saiu em 2014 — e a 5.0 só chegou dez anos depois, em 2024. Uma década com uma única versão maior é raríssimo em JavaScript, e explica um efeito colateral que você vai sentir a semana inteira: praticamente todo tutorial, resposta de fórum e trecho gerado por IA que você encontrar por aí foi escrito para o Express 4. É por isso que a ESPECIFICAÇÃO deste curso tem uma seção só de armadilhas de sintaxe. Hoje o projeto é mantido pela OpenJS Foundation, a mesma fundação que abriga o Node.js.
// Registra no console o método, o caminho, o status e a duração de cada requisição.functionregistrarRequisicao(req,res,next){constinicio=Date.now();// O evento "finish" dispara quando a resposta terminou de ser enviada ao cliente.res.on("finish",()=>{constduracao=Date.now()-inicio;console.log(`${req.method}${req.originalUrl} → ${res.statusCode} (${duracao}ms)`);});next();}module.exports=registrarRequisicao;
Repare no truque: o middleware roda antes da rota, quando o status ainda não existe. Para saber o resultado, ele se inscreve no evento finish de res — o mesmo padrão de addEventListener que você usa no DOM — e só imprime quando a resposta já foi embora.
Saída típica no terminal:
Texto
GET / → 200 (4ms)
GET /css/estilo.css → 200 (1ms)
GET /api/produtos → 200 (2ms)
GET /api/produtos/999 → 404 (1ms)
POST /api/produtos → 201 (3ms)
Quando app.use recebe um caminho como primeiro argumento, o middleware só roda para requisições que começam com aquele caminho:
JavaScript
// Só roda para /api/qualquer-coisaapp.use("/api",(req,res,next)=>{res.setHeader("X-API-Versao","1.0");next();});
Confira com curl -i http://localhost:3000/api/produtos: o cabeçalho X-API-Versao: 1.0 aparece na resposta. Peça curl -i http://localhost:3000/ e ele não aparece.
⚠️ Atenção
Dentro de um middleware montado em um prefixo, req.url vem sem o prefixo: em GET /api/produtos, o req.url vale /produtos. Isso é intencional (é o que permite ao Router ter caminhos relativos), mas estraga mensagens de log e de erro. Para o caminho completo, use sempre req.originalUrl.
Middlewares também podem ser passados como argumentos extras, antes do handler final:
JavaScript
// Confere se o :id é um número inteiro positivo antes de qualquer coisa.functionvalidarId(req,res,next){constid=Number(req.params.id);if(!Number.isInteger(id)||id<=0){returnres.status(400).json({erro:`O id "${req.params.id}" não é um número válido.`});}req.idProduto=id;// deixa pronto para o handler, já convertidonext();}// O middleware roda primeiro; o handler só é chamado se ele der next().router.get("/:id",validarId,(req,res)=>{constproduto=produtos.find((p)=>p.id===req.idProduto);if(!produto)returnres.status(404).json({erro:"Produto não encontrado."});res.json(produto);});
Duas coisas boas aconteceram aí. A conversão e a validação do id saíram de dentro do handler — que agora só cuida da regra dele. E req.idProduto mostra o padrão mais comum de comunicação entre middlewares: pendurar informação no req para quem vier depois. É exatamente assim que o middleware de autenticação da Aula 14 vai entregar req.usuario às rotas.
🔎 Por baixo do capô
O next() não é mágica: o Express guarda a lista de middlewares que casaram com a requisição e mantém um índice interno apontando para o atual. Chamar next() incrementa esse índice e invoca o próximo da lista. Chamar next()duas vezes no mesmo middleware avança duas casas e costuma produzir Error: Cannot set headers after they are sent to the client, porque dois handlers tentam responder. E chamar next(erro) — com um argumento — faz o Express pular todos os middlewares normais restantes e ir direto para o primeiro middleware de erro registrado à frente.
Cinco rotas em um server.js são confortáveis. Quinze já obrigam a rolar a tela para achar coisa. Quarenta, com validações e regras no meio, é um arquivo que ninguém quer abrir — e em que duas pessoas nunca conseguem trabalhar ao mesmo tempo sem conflito no Git.
express.Router() cria um objeto que se comporta como um app em miniatura: aceita .get, .post, .use, middlewares — tudo igual. A diferença é que ele não escuta em porta nenhuma; ele é montado dentro do app principal, sob um prefixo.
routes/produtos.js
JavaScript
constexpress=require("express");constrouter=express.Router();constprodutos=require("../data/produtos.json");// Os caminhos aqui são RELATIVOS ao prefixo onde o router for montado.// Montado em "/api/produtos", este "/" atende GET /api/produtos.router.get("/",(req,res)=>{res.json(produtos);});router.get("/:id",(req,res)=>{constproduto=produtos.find((p)=>p.id===Number(req.params.id));if(!produto){returnres.status(404).json({erro:`Produto ${req.params.id} não encontrado.`});}res.json(produto);});module.exports=router;
constpath=require("node:path");constexpress=require("express");constprodutosRouter=require("./routes/produtos");constapp=express();constPORTA=process.env.PORT||3000;app.use(express.json());app.use(express.static(path.join(__dirname,"public")));app.use("/api/produtos",produtosRouter);// prefixo + routerapp.listen(PORTA,()=>{console.log(`Café Cerrado no ar em http://localhost:${PORTA}`);});
Leia esse arquivo em voz alta: "use o parser de JSON, sirva a pasta public, monte as rotas de produtos em /api/produtos, escute na porta". Ele virou um índice do projeto. Recurso novo — categorias, pedidos, avaliações — é um arquivo novo em routes/ e uma linha aqui.
Repare na linha const produtos = require("../data/produtos.json");. Ela é diferente do que fizemos na Aula 11:
Aula 11
Aula 12
await fs.readFile(...) a cada requisição
require(...) uma vez, na inicialização
Editar o JSON reflete na próxima requisição
O array vive em memória enquanto o servidor roda
Não dá para escrever nada
O POST consegue acrescentar itens ao array
Por que a troca? Porque hoje a API precisa receber um produto, e o array em memória é o lugar mais simples para colocá-lo. A consequência é honesta e precisa ser dita: o que você criar via POST desaparece quando o servidor reinicia. Na próxima aula, com fs/promises e persistência de verdade, isso é resolvido — e é justamente por resolver isso que aquela aula existe.
💡 Dica
O require de um arquivo .json lê e converte o arquivo de forma síncrona, e guarda o resultado em cache: pedir o mesmo arquivo em dois módulos diferentes devolve exatamente o mesmo array na memória, não duas cópias. Isso é ótimo aqui (todo mundo vê a mesma lista) e é uma armadilha clássica quando você espera cópias independentes.
cafe-cerrado-api/
├── data/
│ └── produtos.json # os dados
├── middlewares/
│ ├── erros.js # 404 da API + tratador de erros
│ └── registro.js # log de requisições
├── public/ # o site das Unidades 1 e 2
│ ├── css/
│ ├── img/
│ ├── js/
│ │ └── app.js
│ └── index.html
├── routes/
│ └── produtos.js # todas as rotas de /api/produtos
├── .gitignore
├── package.json
├── package-lock.json
├── server.js # só configuração e montagem
└── testes.http # requisições de teste, versionadas
Compare com a estrutura da Aula 11: as pastas não são enfeite. Cada nome responde a uma pergunta ("onde estão as rotas?", "onde está o log?") sem que ninguém precise abrir arquivo nenhum.
Hoje, se alguém pedir GET /api/pedidos — uma rota que não existe —, o Express responde com uma página HTML dizendo Cannot GET /api/pedidos. Para um navegador, tudo bem. Para um fetch esperando JSON, é péssimo: resposta.json() lança SyntaxError: Unexpected token '<', "<!DOCTYPE "... is not valid JSON, e o erro que aparece no console do usuário não tem nada a ver com o problema real.
A solução é um middleware registrado depois de todas as rotas de API:
middlewares/erros.js (primeira metade)
JavaScript
// Só chega aqui quem pediu algo em /api e não casou com nenhuma rota.functionnaoEncontradoApi(req,res){res.status(404).json({erro:`A rota ${req.method}${req.originalUrl} não existe nesta API.`,});}
JavaScript
// server.js — DEPOIS de app.use("/api/produtos", produtosRouter)app.use("/api",naoEncontradoApi);
E os caminhos que não começam com /api? Como o Café Cerrado é uma SPA (Aula 10), a resposta certa é devolver o index.html e deixar o roteador do front decidir o que mostrar. Para casar com "qualquer caminho", o Express 5 exige um curinga nomeado:
JavaScript
// server.js — depois do 404 da APIapp.get("/{*splat}",(req,res)=>{res.sendFile(path.join(__dirname,"public","index.html"));});
As chaves em /{*splat} tornam o trecho opcional, de modo que o padrão casa também com a raiz /. O nome splat é livre — poderia ser caminho — e o valor capturado fica em req.params.splat, como um array de segmentos.
⚠️ Atenção
No Express 4 escrevia-se app.get("*", handler). No Express 5 isso derruba o servidor na inicialização com TypeError: Missing parameter name at 1. A mudança veio da biblioteca de rotas (path-to-regexp), que passou a exigir nome em todo curinga. Se você copiar um app.get("*") de um tutorial, é este o erro que vai aparecer — e ele acontece antes mesmo de o servidor subir, o que ao menos é fácil de perceber.
O Express reconhece um middleware de erro pela quantidade de parâmetros: quatro, começando por err. Não é pelo nome nem pela posição — é pela aridade da função.
middlewares/erros.js (segunda metade)
JavaScript
// QUATRO parâmetros = middleware de erro. O "next" precisa existir mesmo sem uso.functiontratadorDeErros(err,req,res,next){// Log completo para o desenvolvedor: aparece no terminal do servidor.console.error(`[erro] ${req.method}${req.originalUrl}`);console.error(err);// JSON malformado no corpo: o próprio express.json() marca o erro com status 400.if(err.status===400&&err.type==="entity.parse.failed"){returnres.status(400).json({erro:"O corpo da requisição não é um JSON válido."});}// Resposta genérica para o cliente: nunca exponha detalhes internos.res.status(500).json({erro:"Erro interno do servidor."});}module.exports={naoEncontradoApi,tratadorDeErros};
JavaScript
// server.js — a ÚLTIMA linha antes do app.listenapp.use(tratadorDeErros);
O parâmetro next fica ali sem ser usado, e isso incomoda todo mundo na primeira vez. Ele é obrigatório: retire-o e a função passa a ter três parâmetros, o Express deixa de reconhecê-la como tratador de erros e passa a tratá-la como middleware comum — que nunca vai rodar, porque está registrada depois de todas as rotas.
O Express percorre a fila na ordem de registro. Quando algo lança um erro, ele pula todos os middlewares comuns restantes e procura o próximo middleware de erro à frente na fila — nunca para trás. Registrar o tratador antes das rotas o tornaria inalcançável.
Texto
requisição
│
▼
express.json() ─────► ok, next()
│
▼
registrarRequisicao ► ok, next()
│
▼
produtosRouter ─────► lançou um erro
│ │
│ o Express pula os middlewares comuns
│ e procura o próximo DE ERRO à frente
▼ ▼
naoEncontradoApi tratadorDeErros
(não roda) (roda: responde 500)
A ordem — rotas, depois 404, depois tratador de erros — não é questão de estilo. É a única ordem em que os três cumprem o papel deles.
No Express 5, um throw dentro de um handler async é capturado automaticamente e encaminhado ao tratador de erros. Você viu isso na Aula 11; agora dá para provar, porque o tratador existe:
JavaScript
// Rota de teste: derruba de propósito, para ver o tratador funcionando.// Acrescente-a TEMPORARIAMENTE ao routes/produtos.js, rode o curl abaixo e// apague-a em seguida — ela não entra no arquivo final do Mão na massa.router.get("/teste/erro",async(req,res)=>{thrownewError("Explosão proposital para testar o tratador de erros");});
HTTP/1.1 500 Internal Server Error
Content-Type: application/json; charset=utf-8
{"erro":"Erro interno do servidor."}
E o terminal mostra a mensagem completa com a pilha de chamadas. Esse par — log detalhado no servidor, mensagem genérica ao cliente — é uma regra de segurança, não de organização. Um stack trace devolvido ao navegador entrega o caminho absoluto das suas pastas, os nomes dos seus arquivos e, muitas vezes, o trecho da consulta ao banco. É presente para quem estiver procurando uma brecha.
📌 Vale gravar
Três perguntas que valem a pena revisar sobre esta seção: (1) O que identifica um middleware de erro? Ter quatro parâmetros. (2) Por que ele precisa ser o último registrado? Porque o Express só procura tratadores de erro à frente na fila. (3) O que muda no Express 5 quanto a handlers async? Exceções e Promises rejeitadas passam a ser capturadas automaticamente, sem try/catch nem pacotes auxiliares.
🧩 Padrão de projeto em uso — Chain of Responsibility¶
A fila express.json() → registrarRequisicao → express.static → produtosRouter → naoEncontradoApi → tratadorDeErros é uma implementação do padrão comportamental Chain of Responsibility ("corrente de responsabilidade"): um pedido percorre uma corrente de manipuladores, e cada elo decide se trata o pedido e encerra, ou se o repassa ao próximo. Quem envia a requisição não sabe — e não precisa saber — qual elo vai atendê-la.
Os dois efeitos que você sente na prática vêm direto do padrão. Primeiro, a ordem é o desenho: cada elo só recebe o pedido se o anterior decidiu repassá-lo, e é por isso que trocar duas linhas de app.use muda o comportamento do servidor. Segundo, os elos são independentes: acrescentar autenticação na Aula 14 será encaixar mais um elo na corrente, sem tocar em nenhum dos outros.
Você já viu a mesma ideia no front-end, com outro nome: a propagação de eventos do DOM. Um clique sobe do <button> para o <div> e daí para o document, e qualquer listener no caminho pode tratar o evento e interromper a subida com stopPropagation(). next() e stopPropagation() resolvem o mesmo problema pelos dois lados: um manda seguir, o outro manda parar.
// Registra método, caminho, status e duração de cada requisição atendida.functionregistrarRequisicao(req,res,next){constinicio=Date.now();res.on("finish",()=>{constduracao=Date.now()-inicio;console.log(`${req.method}${req.originalUrl} → ${res.statusCode} (${duracao}ms)`);});next();}module.exports=registrarRequisicao;
// Middleware do 404 da API: registrado DEPOIS de todas as rotas de /api.functionnaoEncontradoApi(req,res){res.status(404).json({erro:`A rota ${req.method}${req.originalUrl} não existe nesta API.`,});}// Middleware de erro: reconhecido pelos QUATRO parâmetros. Vai por último.functiontratadorDeErros(err,req,res,next){console.error(`[erro] ${req.method}${req.originalUrl}`);console.error(err);if(err.status===400&&err.type==="entity.parse.failed"){returnres.status(400).json({erro:"O corpo da requisição não é um JSON válido."});}res.status(500).json({erro:"Erro interno do servidor."});}module.exports={naoEncontradoApi,tratadorDeErros};
constexpress=require("express");constrouter=express.Router();// Carregado uma vez, na inicialização. O array vive em memória enquanto o// servidor roda — a persistência em arquivo chega na próxima aula.constprodutos=require("../data/produtos.json");// Próximo id disponível: o maior id existente mais um.letproximoId=produtos.reduce((maior,p)=>Math.max(maior,p.id),0)+1;// Middleware de rota: valida e converte o :id antes de qualquer handler.functionvalidarId(req,res,next){constid=Number(req.params.id);if(!Number.isInteger(id)||id<=0){returnres.status(400).json({erro:`O id "${req.params.id}" não é um número válido.`});}req.idProduto=id;next();}// GET /api/produtos → lista completa, com filtro opcional por categoriarouter.get("/",(req,res)=>{const{categoria}=req.query;if(!categoria){returnres.json(produtos);}constfiltrados=produtos.filter((p)=>p.categoria.toLowerCase()===categoria.toLowerCase(),);res.json(filtrados);});// GET /api/produtos/:id → um produto, ou 404router.get("/:id",validarId,(req,res)=>{constproduto=produtos.find((p)=>p.id===req.idProduto);if(!produto){returnres.status(404).json({erro:`Produto ${req.idProduto} não encontrado.`});}res.json(produto);});// POST /api/produtos → cria um produto. Validação obrigatória no servidor.router.post("/",(req,res)=>{const{nome,categoria,preco,descricao,imagem}=req.body;constproblemas=[];if(typeofnome!=="string"||nome.trim().length<3){problemas.push("nome deve ser um texto com pelo menos 3 caracteres");}if(typeofcategoria!=="string"||categoria.trim()===""){problemas.push("categoria é obrigatória");}if(typeofpreco!=="number"||Number.isNaN(preco)||preco<=0){problemas.push("preco deve ser um número maior que zero");}if(problemas.length>0){returnres.status(400).json({erro:"Dados inválidos.",problemas});}constnovo={id:proximoId++,nome:nome.trim(),categoria:categoria.trim().toLowerCase(),preco,descricao:typeofdescricao==="string"?descricao.trim():"",imagem:typeofimagem==="string"?imagem.trim():"img/sem-foto.jpg",};produtos.push(novo);res.status(201).json(novo);});module.exports=router;
O GET /api/categorias da Aula 11 não pode sumir na refatoração: o <select> de categoria da SPA depende dele. Ele sai do server.js e vira o segundo Router do projeto — pequeno, mas com o mesmo formato dos demais.
routes/categorias.js
JavaScript
constexpress=require("express");constrouter=express.Router();// O MESMO array que routes/produtos.js carregou: o require guarda o módulo em// cache, então os dois arquivos enxergam o mesmo objeto na memória.constprodutos=require("../data/produtos.json");// As quatro categorias do cardápio, na ordem em que aparecem no site.// Esta lista estava no server.js da Aula 11; ela só mudou de arquivo.constCATEGORIAS=[{id:"cafes",nome:"Cafés"},{id:"geladas",nome:"Bebidas geladas"},{id:"salgados",nome:"Salgados"},{id:"doces",nome:"Doces"},];// GET /api/categorias → [{ id, nome }], só as categorias que têm produto hojerouter.get("/",(req,res)=>{constusadas=newSet(produtos.map((p)=>p.categoria));res.json(CATEGORIAS.filter((categoria)=>usadas.has(categoria.id)));});module.exports=router;
Doze linhas úteis, um recurso inteiro. É essa a promessa do express.Router: recurso novo é arquivo novo, e o server.js cresce uma linha.
constpath=require("node:path");constexpress=require("express");constprodutosRouter=require("./routes/produtos");constcategoriasRouter=require("./routes/categorias");constregistrarRequisicao=require("./middlewares/registro");const{naoEncontradoApi,tratadorDeErros}=require("./middlewares/erros");constapp=express();constPORTA=process.env.PORT||3000;// 1. Corpo JSON das requisições vira objeto em req.bodyapp.use(express.json());// 2. Log de tudo que chegaapp.use(registrarRequisicao);// 3. Arquivos estáticos: o site das Unidades 1 e 2app.use(express.static(path.join(__dirname,"public")));// 4. Rotas da API — um app.use por recursoapp.use("/api/produtos",produtosRouter);app.use("/api/categorias",categoriasRouter);// 5. Qualquer /api que não casou acima vira 404 em JSONapp.use("/api",naoEncontradoApi);// 6. Qualquer outro caminho devolve o index.html (a SPA cuida do resto)app.get("/{*splat}",(req,res)=>{res.sendFile(path.join(__dirname,"public","index.html"));});// 7. Tratador de erros: SEMPRE o últimoapp.use(tratadorDeErros);app.listen(PORTA,()=>{console.log(`Café Cerrado no ar em http://localhost:${PORTA}`);});
Sete blocos comentados, sete responsabilidades, nenhuma regra de negócio. Esse é o formato de server.js que o projeto vai manter até a Aula 16 — inclusive o fallback /{*splat}, que é o que mantém a SPA da Aula 10 funcionando quando alguém recarrega a página em /#/cardapio.
@base = http://localhost:3000### Listar todos os produtos (espera 200)GET {{base}}/api/produtos### Filtrar por categoria (espera 200 com só os cafés)GET {{base}}/api/produtos?categoria=cafes### Buscar um produto existente (espera 200)GET {{base}}/api/produtos/1### Buscar um produto inexistente (espera 404 em JSON)GET {{base}}/api/produtos/999### Id que não é número (espera 400)GET {{base}}/api/produtos/abacaxi### Listar as categorias (espera 200 com objetos {id, nome})GET {{base}}/api/categorias### Criar um produto válido (espera 201 + o recurso criado)POST {{base}}/api/produtosContent-Type: application/json{ "nome": "Suco de Cupuaçu", "categoria": "geladas", "preco": 10.5, "descricao": "Polpa batida na hora com água gelada e um fio de mel.", "imagem": "img/suco-cupuacu.jpg"}### Criar sem nome (espera 400 com a lista de problemas)POST {{base}}/api/produtosContent-Type: application/json{ "categoria": "doces", "preco": 10.5}### Preço como texto (espera 400)POST {{base}}/api/produtosContent-Type: application/json{ "nome": "Café gelado da casa", "categoria": "geladas", "preco": "dez reais"}### JSON malformado (espera 400 vindo do express.json)POST {{base}}/api/produtosContent-Type: application/json{ "nome": "Café",}### Rota de API que não existe (espera 404 em JSON)GET {{base}}/api/pedidos### Caminho fora da API (espera o index.html)GET {{base}}/qualquer-coisa
No VS Code, com o REST Client instalado, aparece um link Send Request acima de cada bloco ###. Clique e a resposta abre ao lado, com status, cabeçalhos e corpo.
💡 Dica@base = http://localhost:3000 é uma variável do REST Client. Quando o projeto for publicado (trilha Deploy), você troca uma linha para testar o servidor de produção. Alternativas ao REST Client: Postman e Insomnia, com interface gráfica. A vantagem de um testes.http é ele ser texto puro: entra no Git, aparece no diff, e quem clonar o repositório recebe a bateria de testes junto com o código.
npm run dev. O terminal mostra Café Cerrado no ar em http://localhost:3000.
Abra http://localhost:3000 e navegue pelo site. O terminal deve imprimir uma linha de log por arquivo carregado, com status e duração.
No testes.http, dispare os blocos de cima para baixo e confira os status: 200, 200, 200, 404, 400, 200, 201, 400, 400, 400, 404, 200.
Depois do POST que devolveu 201, dispare de novo o primeiro bloco: o "Suco de Cupuaçu" está na lista, com o id 11 — o seguinte ao do último produto do cardápio.
Pare o servidor (Ctrl+C), suba de novo e liste outra vez: o suco sumiu. É o comportamento esperado hoje — e o problema que a próxima aula resolve.
Peça curl -i http://localhost:3000/api/pedidos: a resposta é 404 com Content-Type: application/json, não uma página HTML.
Comente a linha app.use(express.json()); do server.js, salve e dispare o POST válido: veja o 500 e a mensagem TypeError: Cannot destructure property 'nome' of 'req.body' as it is undefined. no terminal. Descomente antes de seguir.
Commit:
Terminal
gitadd.
gitcommit-m"Rotas em Router, middlewares de log e erro, POST com validacao e testes.http"
gitpush
A1. Dada a cadeia abaixo, diga por quais middlewares passa cada uma destas três requisições: GET /css/estilo.css, GET /api/produtos/1, GET /api/avaliacoes.
A2. O que acontece com uma requisição que chega neste middleware? Descreva o que o usuário vê no navegador e o que aparece no terminal.
JavaScript
app.use((req,res,next)=>{console.log("passei por aqui");});
A3. Um colega registrou app.use(express.json())depois de app.use("/api/produtos", produtosRouter). O GET continua funcionando e o POST quebra. Explique por quê, em duas linhas.
A4. Qual destas quatro funções o Express trata como middleware de erro? Justifique.
A5. Dentro de um middleware montado com app.use("/api", fn), uma requisição para GET /api/produtos/3 chega com req.url valendo o quê? E req.originalUrl? Qual dos dois você usaria na mensagem do 404 e por quê?
A6. Complete o middleware para que ele bloqueie qualquer requisição cujo corpo seja maior que 10 campos, respondendo 400. Ele deve deixar passar todas as outras.
JavaScript
functionlimitarCampos(req,res,next){constquantidade=Object.keys(req.body??{}).length;// Escreva aqui a verificação e a resposta 400.next();}
B1. Produtos de uma categoria. Acrescente ao routes/categorias.js do Mão na massa a rota GET /api/categorias/:id/produtos, que devolve os produtos daquela categoria — sem tocar no server.js e sem duplicar o filtro que já existe em routes/produtos.js.
Resultado esperado: curl http://localhost:3000/api/categorias/cafes/produtos devolve os quatro cafés; curl http://localhost:3000/api/categorias/geladas/produtos devolve os dois itens gelados; uma categoria inexistente devolve [] com status 200.
Dica
O app.use("/api/categorias", categoriasRouter) já existe desde o Passo 5: dentro do router, o caminho novo é "/:id/produtos". O filtro é o mesmo produtos.filter((p) => p.categoria === req.params.id); se quiser evitar a duplicação de verdade, extraia a comparação para uma função em um arquivo compartilhado e chame-a nos dois routers.
B2. Middleware de autorização simulada. Escreva middlewares/chaveApi.js, que exige o cabeçalho X-Chave-Api: cafe-cerrado-2 em todas as requisições POST de /api. Sem o cabeçalho, responde 401 com { "erro": "..." }; com ele, chama next().
Resultado esperado: o POST do testes.http passa a devolver 401; acrescentando a linha X-Chave-Api: cafe-cerrado-2 ao bloco, volta a devolver 201. Os GET continuam funcionando sem o cabeçalho.
Dica
Cabeçalhos chegam sempre em minúsculas: req.headers["x-chave-api"]. Para agir só no POST, teste req.method !== "POST" e chame next() de imediato. Isto é um ensaio do middleware exigirLogin da Aula 14 — lá a chave vira um token de verdade.
B3. Validação extraída para middleware. Tire o bloco de validação de dentro do POST e transforme-o em um middleware validarProduto registrado como argumento da rota. O handler deve ficar com no máximo dez linhas.
Resultado esperado: os mesmos status de antes (201 e 400 com a lista de problemas), mas o handler do POST só monta o objeto e responde. O middleware fica em middlewares/validarProduto.js.
Dica
router.post("/", validarProduto, (req, res) => { ... }). O middleware pode até normalizar os dados (nome.trim()) e devolvê-los prontos em req.produtoValidado, poupando o handler.
B4. O testes.http completo. Amplie o arquivo para cobrir, além do que já existe, os casos: filtro por categoria inexistente, POST com preço negativo, POST com nome de dois caracteres e requisição a /api/produtos/0. Cada bloco deve ter, no título, o status esperado.
Resultado esperado: quatro blocos novos, todos disparados com sucesso e com o status previsto batendo com o obtido. Nenhum deles derruba o servidor.
Dica
/api/produtos/0 é o caso que separa Number.isInteger(id) de id > 0. Se a sua validação usar só isNaN, o zero passa — e passar zero significa procurar um produto que jamais existirá, gastando um 404 onde cabia um 400.
C1. Corrente completa com prioridade. Implemente um middleware medirLento que registre no console, em destaque, toda requisição que demore mais de 50 ms, e um middleware simularLatencia que — só quando a query string trouxer ?lento=1 — atrase a resposta em 300 ms antes de chamar next(). Depois, prove que a corrente funciona: mostre no terminal um GET /api/produtos normal e um GET /api/produtos?lento=1, e explique por que o simularLatencia precisa estar registrado antes das rotas e o medirLentoantes dele.
Resultado esperado: no terminal, GET /api/produtos → 200 (2ms) e GET /api/produtos?lento=1 → 200 (301ms) [LENTO]; a explicação da ordem em três linhas de comentário no topo do server.js.
Dica
Para atrasar sem travar o event loop, setTimeout(next, 300) — nunca um laço while contando tempo, que bloqueia o processo inteiro e faz o servidor parar de atender todo mundo. Para o medirLento, reaproveite o res.on("finish", ...) do middleware de log: a duração já está calculada ali. E lembre que req.query.lento chega como a string "1", não como número.
Duas pessoas abrem o cardápio ao mesmo tempo e o seu terminal cospe seis linhas de log embaralhadas: não dá para saber quais linhas pertencem a qual visita. Serviços de verdade resolvem isso dando um identificador único a cada requisição, que acompanha a requisição do começo ao fim e volta ao cliente em um cabeçalho — é assim que o suporte de uma empresa pede "me manda o id da requisição" e acha o problema em segundos. Dê um crachá a cada requisição do Café Cerrado.
Critérios de pronto
Um middleware registrado antes de todos os outros gera um identificador único e o guarda em req.id.
Todas as linhas de log daquela requisição — a de entrada e a de saída — começam com o mesmo identificador.
A resposta traz o identificador no cabeçalho X-Requisicao-Id, verificável com curl -i.
O tratador de erros inclui o identificador tanto no log do servidor quanto no JSON devolvido ao cliente.
Um parágrafo no README.md explica em que situação real esse identificador salva tempo.
Pistas
require("node:crypto").randomUUID() gera um identificador único sem instalar nada.
Um UUID inteiro polui o terminal; .slice(0, 8) é suficiente para distinguir requisições de uma aula.
res.setHeader("X-Requisicao-Id", req.id) precisa acontecer antes de qualquer res.json ou res.send — cabeçalhos não podem ser definidos depois do corpo enviado.
Devolver o identificador junto do erro é seguro (é um número aleatório, não revela nada) e é o que permite ao usuário reportar o problema de forma útil.
Um colega mexeu no server.js sem avisar e agora o site "não abre mais": a aba do navegador fica girando indefinidamente, o terminal não mostra erro nenhum e o processo Node continua vivo, sem consumir CPU. O curl também fica pendurado. Nenhum console.log que você acrescentar no handler da rota aparece. Este é o trecho alterado:
Antes de corrigir, diagnostique: por que o site abre e a API não? Ou o contrário? Prove o que está acontecendo antes de tocar no código.
Critérios de pronto
Um arquivo docs/diagnostico-pendente.md registra: o que a aba Network mostra (status e tempo), o que o curl -v mostra, e o que aparece (ou não) no terminal do servidor.
O diagnóstico explica, em três linhas, qual caminho de execução deixa a requisição pendurada e por quê.
A correção mantém o cabeçalho X-API-Versao nas rotas de API e devolve o comportamento normal a todas as requisições.
Um teste no testes.http prova que /api/produtos responde 200com o cabeçalho e que / responde 200 sem ele.
Uma frase no arquivo explica por que esse tipo de bug não gera nenhuma mensagem de erro.
Pistas
Leia o if com calma: em qual dos dois ramos o next() é chamado?
curl -v --max-time 5 http://localhost:3000/api/produtos desiste em cinco segundos e mostra exatamente onde travou.
Definir um cabeçalho não envia a resposta. Só res.json, res.send, res.end e companhia encerram o ciclo.
A correção certa não é acrescentar um res.end() no ramo do if — pense em qual das três coisas que um middleware pode fazer está faltando ali.
Hoje a validação do POST /api/produtos são vinte linhas de if dentro do handler. Na próxima aula chega o PUT, que precisa das mesmas regras. Na aula seguinte, o recurso de categorias. Copiar e colar esses if três vezes é a receita conhecida para o dia em que a regra mudar em dois lugares e ficar esquecida no terceiro. Escreva um middleware genérico que receba a descrição do que validar e devolva o middleware pronto — e prove que ele serve para recursos diferentes.
Critérios de pronto
middlewares/validar.js exporta uma função validar(regras) que retorna um middleware.
As regras de produto ficam declaradas em um objeto, fora do middleware: campo, tipo esperado, obrigatoriedade e uma condição extra opcional.
O POST /api/produtos usa validar(regrasProduto) e o handler fica com no máximo dez linhas.
Um 400 traz a lista de todos os problemas encontrados, não só o primeiro.
Um segundo recurso (categorias, avaliações, o que você preferir) usa o mesmo validar com outro objeto de regras, sem alterar uma linha do middleware.
Pistas
Uma função que devolve um middleware é o padrão de fábrica que o próprio Express usa: express.json() também é uma chamada que retorna a função (req, res, next).
Object.entries(regras) percorre campo por campo; acumule as mensagens em um array e responda uma vez só, no fim.
Isto é o padrão Strategy aplicado: o algoritmo (o conjunto de regras) é injetado de fora, e o middleware não sabe nada sobre produtos. É também a ideia por trás de bibliotecas como zod e Joi — depois de fazer o seu, vale ler a documentação de uma delas para comparar.
app.use(express.json()) parece uma linha mágica: o corpo cru vira objeto e ninguém pergunta como. Não é mágica — é um stream. O corpo de uma requisição HTTP chega em pedaços, e alguém precisa juntá-los, decidir quando acabou, verificar o Content-Type, converter e ainda impedir que uma requisição de 2 GB acabe com a memória do servidor. Escreva esse alguém, e depois compare o seu resultado com o original.
Critérios de pronto
middlewares/meuJson.js exporta uma função que, registrada no lugar de express.json(), faz o POST de produtos funcionar exatamente como antes.
O middleware só age quando o cabeçalho Content-Type é application/json; nos outros casos chama next() sem tocar em req.body.
Corpo com JSON inválido produz 400 com uma mensagem clara, sem derrubar o servidor.
Corpo maior que um limite configurável (por exemplo, 100 kB) é recusado com 413 Payload Too Large, e a conexão é encerrada antes de o restante ser lido.
Um documento docs/meu-json.md compara o seu middleware com o express.json() em pelo menos quatro aspectos, e explica o que o express.json() faz que o seu não faz.
Uma medição: curl enviando um corpo de 1 MB é recusado com 413, e o log mostra quantos bytes chegaram a ser lidos antes da recusa.
Pistas
req é um stream legível. O padrão é req.on("data", (pedaco) => {...}) acumulando em um array, req.on("end", () => {...}) para finalizar e req.on("error", next) para não engolir falhas de rede.
Acumule Buffer (não string) e junte no fim com Buffer.concat(pedacos).toString("utf-8") — concatenar strings pedaço a pedaço quebra caracteres acentuados que caíram na fronteira entre dois pedaços. Esse bug é sutil e vale muito descobri-lo na prática.
Some pedaco.length a cada evento data; ao passar do limite, responda 413 e chame req.destroy() para não continuar recebendo.
req.headers["content-type"] pode vir como application/json; charset=utf-8 — compare com .startsWith("application/json"), não com igualdade.
Para gerar 1 MB de corpo: node -e 'process.stdout.write(JSON.stringify({t:"x".repeat(1e6)}))' > grande.json e depois curl -X POST --data-binary @grande.json -H "Content-Type: application/json" http://localhost:3000/api/produtos.
No seu projeto autoral, aplique a mesma refatoração:
Crie routes/<seu-recurso>.js com um express.Router contendo todas as rotas do recurso, e deixe o server.js só com a montagem das peças.
Crie middlewares/registro.js com o log de método, caminho, status e duração.
Crie middlewares/erros.js com o 404 de API em JSON e o tratador de erros de quatro parâmetros, registrados na ordem certa.
Acrescente express.json() e implemente POST /api/<seu-recurso> com validação no servidor, devolvendo 201 no sucesso e 400 com a lista de problemas no erro.
Acrescente o curinga /{*splat} devolvendo o index.html, para o front continuar funcionando em qualquer caminho.
Crie o testes.http na raiz, cobrindo no mínimo: listagem, item existente, item inexistente (404), rota de API inexistente (404), POST válido (201) e POST inválido (400).
Critério de pronto: todos os blocos do testes.http disparam com o status esperado, o terminal mostra uma linha de log por requisição e o server.js tem menos de trinta linhas.
QUEIRÓS, Ricardo; PORTELA, Filipe. Introdução ao Desenvolvimento Moderno para a Web. FCA — organização e estruturação da camada de back-end.
LOUDON, Kyle. Desenvolvimento de Grandes Aplicações Web. Novatec — modularidade e código que precisa crescer sem virar espaguete.
ALVES, William P. Projetos de Sistemas Web. Érica — arquitetura de sistemas web em camadas.
Na próxima aula damos o último passo da arquitetura: a rota deixa de conter a lógica e passa a apenas apontar para um controlador. Com controllers/produtosController.js no lugar, o CRUD fica completo — PUT e DELETE se juntam ao GET e ao POST —, a busca por query string ganha corpo e, principalmente, o que você criar para de sumir: os dados passam a ser gravados de verdade em data/produtos.json com fs/promises.