Dia 6 — Rotas
Ler a URL e separar os caminhos
Ler a URL e separar os caminhos
req.url chega como uma string só. O cliente manda
/alunos?turma=3&ordem=nome e o servidor é quem precisa tirar as duas partes:
o caminho diz qual rota é, a query string diz os parâmetros.
Separar isso com split('?') funciona até o dia em que um valor tem ? dentro
(?busca=a?b), e aí o resultado é silenciosamente errado. URL e
URLSearchParams existem para isso.
As duas APIs antigas ainda são o que a maior parte da documentação e do Stack
Overflow mostra. url.parse é o método do módulo node:url que devolve um
objeto com pathname e query, e querystring é o módulo que fazia a
segunda parte do trabalho — querystring.parse(u.query). As duas existem e
funcionam, mas cada uma erra por um motivo diferente, e URL erra por um só,
que é não existir.
const u = new URL(req.url, 'http://127.0.0.1:3000'); u.pathname; // '/alunos' u.searchParams.get('turma'); // '3'
O segundo argumento é a base, e é o que o navegador usa para transformar
/alunos em endereço absoluto. No servidor, a base é o próprio endereço.
pathname e search
| Propriedade | O que devolve |
|---|---|
u.pathname | só o caminho: /alunos/7 |
u.search | a query string inteira, com ?: ?turma=3 |
u.searchParams | os parâmetros já separados |
u.searchParams.size | quantos parâmetros (chaves repetidas contam uma vez) |
O searchParams é o que resolve parâmetro de URL: um nome, um valor, sempre
texto, e a decodificação já feita (%20 já chegou como espaço).
pathname é a base de todo roteamento: a aula 2 de hoje é o if que compara
pathname com os caminhos e escolhe a resposta. O caminho de raiz, /, é a
rota inicial de qualquer servidor — é ele que responde quando alguém digita o
endereço sem pedir nada, e quase sempre é a que devolve a lista de rotas ou um
200 confirmando que a API está de pé.
get devolve null, e Number(null) é 0
Esse é o defeito que só aparece com dado faltando, e é a pegadinha mais cara de
validação de query string.
u.searchParams.get('turma') // '3' — texto, sempre string u.searchParams.get('inexistente') // null — não string vazia Number(null) // 0 — não NaN Number('') // 0 — idem Number('abc') // NaN
A validação que só testa Number.isInteger(Number(valor)) deixa passar o
parâmetro ausente: Number(null) é 0, que é inteiro, e o pedido vai para o
banco filtrando turma = 0. O que funciona é testar a ausência antes de
converter:
const t = u.searchParams.get('turma'); if (t === null || !/^\d+$/.test(t)) return respostaDeErro(400);
get e getAll: parâmetro repetido
?tag=node&tag=mysql tem dois valores com a mesma chave. get('tag') devolve o
primeiro; getAll('tag') devolve os dois, em array. Escolher entre os dois é
decisão de API, e é por isso que a escolha precisa ser explícita no desenho da
rota.
URLSearchParams montando de volta
O mesmo objeto serve para construir query string: set, append, delete,
has, entries e toString(). O toString() escapa os valores sozinho: um espaço vira + (que o servidor
decodifica de volta para espaço) e um & dentro do valor vira %26 — que é
exatamente a pegadinha do ?busca=a?b que abriu esta aula. Não se escapa à mão.
Exemplo
'use strict'; // Exemplo da aula 1 do dia 6: ler a URL e separar caminho de query string. // // `req.url` nao vem separado. O cliente manda uma coisa so — `"/alunos?turma=3&ordem=nome"` — // e e o servidor que precisa tirar as duas partes. `URL` e `URLSearchParams` fazem // essa separacao sem(regex, e sem `split('?')` manual que quebra quando a query // tem `?` dentro do valor. // // O que o exemplo prova, alem do parsing: `searchParams.get` devolve `null` para // parametro ausente (e nao `undefined`, e nao string vazia), e `Number()` de um // `null` vira `0` em vez de `NaN` — a pegadinha classica de validacao. const http = require('node:http'); async function main() { const servidor = http.createServer((req, res) => { // O caminho cru, exatamente como o cliente mandou. res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' }); res.end(req.url); }); await new Promise((resolve) => servidor.listen(0, '127.0.0.1', resolve)); const { port } = servidor.address(); const base = `http://127.0.0.1:${port}`; try { console.log('servidor no ar em ' + base); console.log('(a porta muda a cada execucao: o `listen(0)` pediu uma livre)'); // --- 1. `req.url` cru: uma string so --- const pedidos = [ '/', '/alunos', '/alunos/7', '/alunos?turma=3&ordem=nome', '/alunos?turma=3&ordem=nome&limite=10&offset=20', '/alunos?turma=3&busca=ana%20maria&sinal=%26%3F', '/alunos?turma=', '/alunos?turma=abc', '/alunos?busca=a?b', ]; console.log(''); console.log('--- o que o req.url traz, caminho e query string misturados ---'); for (const caminho of pedidos) { const r = await fetch(base + caminho); console.log(' ' + (caminho === '/' ? '/' : caminho).padEnd(46) + ' -> ' + await r.text()); } // --- 2. `new URL`: separa o que e o que --- // // `new URL(requisicao, base)` exige a URL completa. O `base` e o que o // navegador usa para transformar `/alunos` em endereco absoluto — e aqui e // o endereco do proprio servidor. console.log(''); console.log('--- new URL separa o caminho da query string ---'); for (const caminho of pedidos) { const u = new URL(caminho, base); console.log(' ' + caminho.padEnd(46)); console.log(' pathname ' + u.pathname); console.log(' search ' + u.search); console.log(' searchParams.size ' + u.searchParams.size + ' | params ' + JSON.stringify([...u.searchParams])); } // --- 3. `searchParams`: o parametro por nome --- const u = new URL('/alunos?turma=3&ordem=nome&limite=10&busca=ana%20maria', base); console.log(''); console.log('--- searchParams ---'); console.log("get('turma'): ", u.searchParams.get('turma'), ' <- texto, sempre string'); console.log("get('limite'): ", u.searchParams.get('limite')); console.log("get('ordem'): ", u.searchParams.get('ordem')); console.log("get('busca'): ", JSON.stringify(u.searchParams.get('busca')), ' <- o %20 ja virou espaco'); console.log("get('inexistente'):", u.searchParams.get('inexistente'), ' <- null, nao string vazia'); console.log("has('turma'): ", u.searchParams.has('turma'), "| has('inexistente'):", u.searchParams.has('inexistente')); // `get` devolve a PRIMEIRA ocorrencia; `getAll` devolve todas. E a diferenca // entre `?tag=a&tag=b` lido de duas formas. const repetido = new URL('/alunos?tag=node&tag=mysql&tag=http', base); console.log("get('tag'): ", repetido.searchParams.get('tag'), ' <- so o primeiro'); console.log("getAll('tag'): ", JSON.stringify(repetido.searchParams.getAll('tag'))); // --- 4. a pegadinha: `Number(null)` e `Number('')` --- // // Um parametro ausente devolve `null`, e `Number(null)` e `0` — nao `NaN`. // A validacao que so testa `NaN` deixa passar o 0 e devolve "turma 0" para // quem nao mandou nada. E o defeito que so aparece com dado faltando. console.log(''); console.log('--- a pegadinha do parametro ausente ---'); const semTurma = new URL('/alunos', base); const valor = semTurma.searchParams.get('turma'); console.log("get('turma') sem mandar: ", valor, '(tipo ' + typeof valor + ')'); console.log('Number(valor): ', Number(valor), '<-- 0, e nao NaN'); console.log('Number(null): ', Number(null)); console.log("Number(''): ", Number(''), '<-- tambem 0'); console.log("Number('abc'): ", Number('abc'), '<-- este sim e NaN'); console.log(''); console.log('a validacao que SO testa se virou numero, e que por isso passa:'); console.log(' Number.isInteger(Number(valor)) ->', Number.isInteger(Number(valor)), ' <-- true, e o pedido segue para o banco filtrando turma = 0'); console.log(''); console.log('a validacao que testa a AUSENCIA antes de converter:'); console.log(' Number.isInteger(valor) ->', Number.isInteger(valor), ' <-- false, porque null nao e inteiro'); console.log(' Number(null) === 0:', Number(null) === 0, '<-- a razao de a primeira passar'); console.log(' em codigo: if (t === null || !/^[0-9]+$/.test(t)) -> 400'); // --- 5. montar query string de volta --- const nova = new URLSearchParams(); nova.set('turma', '3'); nova.set('ordem', 'nome'); nova.set('busca', 'ana maria'); console.log(''); console.log('--- URLSearchParams montando de volta ---'); console.log('nova.toString():', nova.toString()); // `+` NAO e o mesmo que `%20` para o `decodeURIComponent`, que nao sabe // desse atalho. Quem sabe e o proprio `URLSearchParams`, e e ele que o // servidor usa para ler: o valor volta igual. const relido = new URLSearchParams(nova.toString()); console.log('o espaco saiu como "+"; relendo com URLSearchParams:', JSON.stringify(relido.get('busca'))); // E o `&` dentro do valor, que e o caso perigoso: sem escapar ele viraria // um parametro novo e o servidor leria `busca=ana` + ` maria` sem valor. const perigoso = new URLSearchParams(); perigoso.set('busca', 'ana & maria'); console.log('com & dentro do valor:', perigoso.toString()); console.log(' o servidor le de volta:', JSON.stringify(new URLSearchParams(perigoso).get('busca'))); console.log('tem get/set/delete/has/entries, e e o mesmo objeto dos dois lados'); } finally { await new Promise((resolve) => servidor.close(resolve)); console.log(''); console.log('servidor encerrado com close().'); } } 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:44921
(a porta muda a cada execucao: o `listen(0)` pediu uma livre)
--- o que o req.url traz, caminho e query string misturados ---
/ -> /
/alunos -> /alunos
/alunos/7 -> /alunos/7
/alunos?turma=3&ordem=nome -> /alunos?turma=3&ordem=nome
/alunos?turma=3&ordem=nome&limite=10&offset=20 -> /alunos?turma=3&ordem=nome&limite=10&offset=20
/alunos?turma=3&busca=ana%20maria&sinal=%26%3F -> /alunos?turma=3&busca=ana%20maria&sinal=%26%3F
/alunos?turma= -> /alunos?turma=
/alunos?turma=abc -> /alunos?turma=abc
/alunos?busca=a?b -> /alunos?busca=a?b
--- new URL separa o caminho da query string ---
/
pathname /
search
searchParams.size 0 | params []
/alunos
pathname /alunos
search
searchParams.size 0 | params []
/alunos/7
pathname /alunos/7
search
searchParams.size 0 | params []
/alunos?turma=3&ordem=nome
pathname /alunos
search ?turma=3&ordem=nome
searchParams.size 2 | params [["turma","3"],["ordem","nome"]]
/alunos?turma=3&ordem=nome&limite=10&offset=20
pathname /alunos
search ?turma=3&ordem=nome&limite=10&offset=20
searchParams.size 4 | params [["turma","3"],["ordem","nome"],["limite","10"],["offset","20"]]
/alunos?turma=3&busca=ana%20maria&sinal=%26%3F
pathname /alunos
search ?turma=3&busca=ana%20maria&sinal=%26%3F
searchParams.size 3 | params [["turma","3"],["busca","ana maria"],["sinal","&?"]]
/alunos?turma=
pathname /alunos
search ?turma=
searchParams.size 1 | params [["turma",""]]
/alunos?turma=abc
pathname /alunos
search ?turma=abc
searchParams.size 1 | params [["turma","abc"]]
/alunos?busca=a?b
pathname /alunos
search ?busca=a?b
searchParams.size 1 | params [["busca","a?b"]]
--- searchParams ---
get('turma'): 3 <- texto, sempre string
get('limite'): 10
get('ordem'): nome
get('busca'): "ana maria" <- o %20 ja virou espaco
get('inexistente'): null <- null, nao string vazia
has('turma'): true | has('inexistente'): false
get('tag'): node <- so o primeiro
getAll('tag'): ["node","mysql","http"]
--- a pegadinha do parametro ausente ---
get('turma') sem mandar: null (tipo object)
Number(valor): 0 <-- 0, e nao NaN
Number(null): 0
Number(''): 0 <-- tambem 0
Number('abc'): NaN <-- este sim e NaN
a validacao que SO testa se virou numero, e que por isso passa:
Number.isInteger(Number(valor)) -> true <-- true, e o pedido segue para o banco filtrando turma = 0
a validacao que testa a AUSENCIA antes de converter:
Number.isInteger(valor) -> false <-- false, porque null nao e inteiro
Number(null) === 0: true <-- a razao de a primeira passar
em codigo: if (t === null || !/^[0-9]+$/.test(t)) -> 400
--- URLSearchParams montando de volta ---
nova.toString(): turma=3&ordem=nome&busca=ana+maria
o espaco saiu como "+"; relendo com URLSearchParams: "ana maria"
com & dentro do valor: busca=ana+%26+maria
o servidor le de volta: "ana & maria"
tem get/set/delete/has/entries, e e o mesmo objeto dos dois lados
servidor encerrado com close().
if/else de rotas e resposta JSON
if/else de rotas e resposta JSON
Um servidor que responde 200 para qualquer caminho não serve para nada. O
roteamento é o que transforma o req em uma resposta útil: o caminho decide
qual função roda, e o método decide se ela aceita aquele pedido.
A ordem: caminho primeiro, metodo depois
A ordem das checagens é o que importa, e o inverso é o defeito clássico. O
if que compara o pathname é o if de rota, e ele vem antes de qualquer
comparação de método.
const rota = rotas[caminho]; if (!rota) return responder(res, 404, { erro: 'rota nao encontrada' }); if (!rota[metodo]) return responder(res, 405, { erro: 'metodo nao permitido' }); return rota[metodo](corpo);
Testar o método antes do caminho junta /alunos com GET e /professores com
POST no mesmo if (req.method === 'POST'), e o segundo pedido recebe a
resposta da rota errada. Caminho primeiro, método depois, sempre.
| Situação | Status |
|---|---|
| o caminho não existe | 404, com o caminho no corpo |
| o caminho existe, o método não | 405, com Allow: GET, POST |
| o caminho e o método existem, mas o dado está errado | 400 |
| deu certo e criou recurso | 201 |
O 405 é o único status do grupo que tem cabeçalho próprio: o Allow diz quais
métodos aquela rota aceita, e é ele que faz o curl sugerir o próximo comando.
O 404 é o status 404 no servidor — o caminho não está na tabela, e a resposta
precisa dizer qual caminho chegou, senão quem está testando não sabe o que
digitou de errado.
O outro lado do par é o método POST, que é o que cria: o mesmo caminho /alunos
responde GET para a lista e POST para a criação, e são duas funções
diferentes na mesma entrada da tabela de rotas.
res.json não existe no módulo nativo
res.json é do Express, não do node:http. Nativo, a resposta JSON são três
linhas:
const texto = JSON.stringify(corpo); res.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8', 'Content-Length': Buffer.byteLength(texto), }); res.end(texto);
O Content-Type é o que faz o cliente entender que aquilo é JSON. Sem ele, o
fetch precisa de .text() e o navegador baixa o arquivo em vez de mostrar.
O Content-Length em Buffer.byteLength conta bytes, não caracteres — texto
com acento ocupa mais de um byte, e é por isso que texto.length dá número
errado.
O par Content-Type e Content-Length é o cabeçalho de resposta que toda API
devolve, e ele vai no writeHead porque precisa sair na mesma hora em que o
status sai: cabeçalho escrito depois do corpo já chegou ao cliente não corrige
nada.
writeHead e statusCode dizem o mesmo
res.writeHead(404, { ... }); // caminho curto res.statusCode = 404; // mesma resposta res.setHeader('Content-Type', 'application/json'); res.end(texto);
O writeHead é o caminho curto. O statusCode separado só é útil quando o
status é decidido longe do ponto onde a resposta é escrita — que é o caso de
try/catch e do finally do servidor.
Uma função por rota que devolve { status, corpo }
O padrão que o material usa daqui em diante: cada rota devolve status e corpo,
e quem escreve na resposta é o roteador. Isso dá dois benefícios reais — a mesma
rota pode ser testada sem subir servidor, e a lista de rotas vira uma lista de
funções que dá para imprimir e conferir.
O par caminho + método é o endpoint: GET /alunos/7 é um endpoint, e o mesmo
caminho com DELETE é outro. É o termo que documentação, teste e cliente usam
para nomear a mesma coisa.
Com uma tabela de rotas não existe switch de rota: o switch (caminho) com
case para cada caminho funciona, mas cresce de um case por endpoint e
ignora o método. A tabela resolve os dois de uma vez, porque a chave é o
caminho e o valor é o objeto de métodos.
const resultado = await rotas['/alunos'].POST({ nm_aluno: 'Diego' }); // { status: 201, corpo: { id: 4, nm_aluno: 'Diego' } }
Ler o corpo da requisição — o on('data'), o JSON.parse e o corpo vazio — é
a aula 1 do dia 7. Enquanto isso, req.corpo é null, e por isso um POST que
manda nm_aluno ainda volta 400: a validação está certa, o dado é que não
chegou.
Exemplo
'use strict'; // Exemplo da aula 2 do dia 6: o roteador, com `if` e resposta JSON. // // Um servidor que responde 200 para tudo e um servidor que nao serve para nada. // O roteamento e o que transforma o `req` em uma resposta util: o caminho // decide qual funcao roda, o metodo decide se ela aceita aquele metodo, e o // `JSON.stringify` entrega o resultado no formato que o cliente espera. // // A ordem das checagens e o que importa: caminho primeiro, metodo depois. E o // inverso (testar o metodo antes do caminho) e o defeito classico: `/alunos` com // GET e `/professores` com POST caem no mesmo `if (req.method === 'POST')` e o // segundo devolve a resposta da rota errada. const http = require('node:http'); async function main() { // Os dados ficam aqui em memoria porque a aula e sobre ROTA, e o banco entra // no dia 12. Uma tabela com duas linhas e o suficiente para a resposta JSON // mostrar a forma. const alunos = [ { id: 1, nm_aluno: 'Ana', turma: 3 }, { id: 2, nm_aluno: 'Bruno', turma: 3 }, { id: 3, nm_aluno: 'Carla', turma: 4 }, ]; // Uma funcao por rota. Devolve { status, corpo } e NAO escreve na resposta: // quem escreve e o roteador, e e assim que o mesmo objeto serve para o // `res.json` do Express de um dia qualquer. const rotas = { '/alunos': { GET: () => ({ status: 200, corpo: { total: alunos.length, dados: alunos } }), // `async` porque toda rota do material e assincrona: quando ela tocar o // banco (dia 12) a consulta entra aqui e devolve promessa. O roteador // espera, e por isso a resposta sai na mesma ordem de todo mundo. POST: async (corpo) => { if (!corpo || !corpo.nm_aluno) { return { status: 400, corpo: { erro: 'nm_aluno e obrigatorio' } }; } return { status: 201, corpo: { id: alunos.length + 1, nm_aluno: corpo.nm_aluno } }; }, }, '/saude': { GET: () => ({ status: 200, corpo: { ok: true } }), }, }; // 404 e 405 ficam no roteador, nao nas rotas: sao as duas respostas que o // servidor deve dar quando NAO achou quem atender. const roteador = async (req, res) => { const caminho = new URL(req.url, 'http://127.0.0.1').pathname; const metodo = req.method; const rota = rotas[caminho]; // --- caminho primeiro --- if (!rota) { return responder(res, 404, { erro: 'rota nao encontrada', caminho, metodo, }); } // --- metodo depois --- if (!rota[metodo]) { res.writeHead(405, { 'Content-Type': 'application/json; charset=utf-8', Allow: Object.keys(rota).join(', ') }); return res.end(JSON.stringify({ erro: 'metodo nao permitido', permitido: Object.keys(rota) })); } // A rota e assincrona e devolve promessa: o roteador espera e so entao // escreve na resposta. Sem o `await`, o `res.end` sairia antes da consulta // ao banco e o cliente leria uma resposta vazia. const resultado = await rota[metodo](await corpoDoPedido(req)); responder(res, resultado.status, resultado.corpo); }; // O corpo do POST chega como texto e precisa virar objeto: e a aula 1 do dia 7 // que faz isso direito. Aqui o roteador so precisa passar o que tem, e a rota // decide se usa. function corpoDoPedido(req) { return Promise.resolve(req.corpo || null); } // `res.json` nao existe no modulo nativo: e `writeHead` + `JSON.stringify` + // `end`, e o `Content-Type` que faz o cliente entender que aquilo e JSON. Sem // o cabecalho, o `fetch` precisa de `.text()` e o navegador baixa o arquivo. function responder(res, status, corpo) { const texto = JSON.stringify(corpo); res.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8', 'Content-Length': Buffer.byteLength(texto), }); res.end(texto); } const servidor = http.createServer(roteador); await new Promise((resolve) => servidor.listen(0, '127.0.0.1', resolve)); const { port } = servidor.address(); const base = `http://127.0.0.1:${port}`; try { console.log('servidor no ar em ' + base); console.log('(a porta muda a cada execucao: o `listen(0)` pediu uma livre)'); console.log('rotas declaradas:', Object.keys(rotas).join(', ')); // --- 1. rota que existe --- console.log(''); console.log('--- GET /alunos (200) ---'); let r = await fetch(base + '/alunos'); console.log('status:', r.status, '| content-type:', r.headers.get('content-type')); console.log('corpo:', await r.text()); // --- 2. caminho que nao existe: 404 --- console.log(''); console.log('--- GET /nao-existe (404) ---'); r = await fetch(base + '/nao-existe'); console.log('status:', r.status); console.log('corpo:', await r.text()); // --- 3. caminho que existe, metodo que nao: 405 --- // // Este e o caso que o roteador de "caminho primeiro, metodo depois" trata // certo. `DELETE /alunos` existe como caminho e nao como metodo, e a // resposta tem de ser 405 com o header `Allow` — nao 404, porque o caminho // existe; e nao 200, porque o metodo nao e aceito. console.log(''); console.log('--- DELETE /alunos (405, o caminho existe) ---'); r = await fetch(base + '/alunos', { method: 'DELETE' }); console.log('status:', r.status, '| header Allow:', r.headers.get('allow')); console.log('corpo:', await r.text()); // --- 4. POST sem o campo obrigatorio: 400 --- console.log(''); console.log('--- POST /alunos sem nm_aluno (400) ---'); // Este POST manda corpo, mas o roteador ainda NAO le o corpo: a aula 1 do // dia 7 monta o `on('data')`. Ate la, `corpoDoPedido` devolve `null` e a // validacao de `nm_aluno` e o que se ve acontecer. r = await fetch(base + '/alunos', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ turma: 3 }), }); console.log('status:', r.status); console.log('corpo:', await r.text()); // --- 5. a MESMA rota, chamada direto, agora com o corpo --- // // Os dois POST acima mandaram `nm_aluno` diferente e os dois voltaram 400, // porque o roteador ainda NAO le o corpo da requisicao — o `req.corpo` e // `null` ate a aula 1 do dia 7 montar o `on('data')`. // // A rota em si nao tem esse defeito: ela funciona, e e o que a chamada // direta mostra. E e o motivo do padrao "rota devolve { status, corpo }": a // rota pode ser testada sem subir servidor nenhum, e sem `fetch`. console.log(''); console.log('--- a rota POST chamada direto, com o corpo na mao ---'); const comNome = await rotas['/alunos'].POST({ nm_aluno: 'Diego', turma: 3 }); console.log('status:', comNome.status, '(201 = criou; 200 seria "deu certo, sem criar nada")'); console.log('corpo:', JSON.stringify(comNome.corpo)); const semNome = await rotas['/alunos'].POST({ turma: 3 }); console.log('sem nm_aluno -> status:', semNome.status, '| corpo:', JSON.stringify(semNome.corpo)); console.log(''); console.log('e o `Allow` da rota 405 vem da mesma fonte:', Object.keys(rotas['/alunos']).join(', ')); // --- 6. `res.statusCode` e `writeHead` dizem o mesmo --- console.log(''); console.log('--- os dois jeitos de mandar o status ---'); console.log('res.writeHead(404, { ... }); res.end(corpo)'); console.log('res.statusCode = 404; res.setHeader(...); res.end(corpo)'); console.log('os dois produzem a mesma resposta; o writeHead e o caminho curto.'); // --- 7. o header `Allow` e a resposta bemEducada do 405 --- console.log(''); console.log('--- 405 e o unico status que tem header proprio ---'); console.log('ele diz QUAIS metodos a rota aceita, e e o que o `Allow` acima transportou'); } finally { await new Promise((resolve) => servidor.close(resolve)); console.log(''); console.log('servidor encerrado com close().'); } } 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:40401
(a porta muda a cada execucao: o `listen(0)` pediu uma livre)
rotas declaradas: /alunos, /saude
--- GET /alunos (200) ---
status: 200 | content-type: application/json; charset=utf-8
corpo: {"total":3,"dados":[{"id":1,"nm_aluno":"Ana","turma":3},{"id":2,"nm_aluno":"Bruno","turma":3},{"id":3,"nm_aluno":"Carla","turma":4}]}
--- GET /nao-existe (404) ---
status: 404
corpo: {"erro":"rota nao encontrada","caminho":"/nao-existe","metodo":"GET"}
--- DELETE /alunos (405, o caminho existe) ---
status: 405 | header Allow: GET, POST
corpo: {"erro":"metodo nao permitido","permitido":["GET","POST"]}
--- POST /alunos sem nm_aluno (400) ---
status: 400
corpo: {"erro":"nm_aluno e obrigatorio"}
--- a rota POST chamada direto, com o corpo na mao ---
status: 201 (201 = criou; 200 seria "deu certo, sem criar nada")
corpo: {"id":4,"nm_aluno":"Diego"}
sem nm_aluno -> status: 400 | corpo: {"erro":"nm_aluno e obrigatorio"}
e o `Allow` da rota 405 vem da mesma fonte: GET, POST
--- os dois jeitos de mandar o status ---
res.writeHead(404, { ... }); res.end(corpo)
res.statusCode = 404; res.setHeader(...); res.end(corpo)
os dois produzem a mesma resposta; o writeHead e o caminho curto.
--- 405 e o unico status que tem header proprio ---
ele diz QUAIS metodos a rota aceita, e e o que o `Allow` acima transportou
servidor encerrado com close().