Dia 7 — Receber e validar dados do cliente

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

Receber e validar dados do cliente

Aula 1

Ler o corpo da requisição

Ler o corpo da requisição

req.url e req.headers vêm prontos. O corpo não. Ele chega como um

stream: um pedaço de cada vez, na ordem em que a rede entregou. Quem monta o

body da requisição é o próprio servidor, ouvindo o evento data.

on('data'), concat, on('end')

function lerCorpo(req) {
  return new Promise((resolve, reject) => {
    const chunks = [];
    req.on('data', (pedaco) => chunks.push(pedaco));
    req.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
    req.on('error', reject);
  });
}

Os chunks são Buffer, não string: o certo é juntar os buffers e converter

uma vez, no end. Fazer texto += pedaco.toString() converte a cada pedaço

e refaz a conversão do texto inteiro a cada rodada.

Para um corpo pequeno, o Node entrega tudo num data só. Para um corpo de

centenas de KB, ele quebra em vários — e a montagem é a mesma. Por isso o

content-length declarado e os bytes contados precisam bater: quando o cliente

manda menos do que promete, o end chega antes e o content-length é maior que

o corpo real.

Corpo vazio: JSON.parse('') lança

Este é o erro mais comum de API: a rota espera objeto, o cliente mandou nada, e

o JSON.parse do body estoura com um SyntaxError que não diz nada sobre o que

o cliente fez de errado.

if (!texto) return respostaDeErro(res, 400, 'corpo vazio');
try { const objeto = JSON.parse(texto); } catch (erro) { /* ... */ }

O teste do texto vem antes do parse, sempre.

Corpo que não é JSON: o erro.message diz a posição

JSON.parse lança SyntaxError cujo message traz o caractere em que o parse

parou. É a única pista de onde o texto virou lixo, e ela aponta o caracter exato.

Texto que parece JSON e não é quase sempre é aspas faltando ou vírgula sobrando.

O servidor não adivinha o tipo

O servidor não sabe se o corpo é JSON, formulário ou binário. Ele só sabe o que

o cabeçalho content-type diz — e o parse é decisão de cada rota. Dois

pedidos com o mesmo corpo e content-type diferentes chegam idênticos ao

servidor; o que muda o comportamento é a rota que os atende.

for await no corpo

req é um stream legível, então dá para percorrer direto:

for await (const pedaco of req) { total += pedaco.length; }

Esse for await é o que a documentação chama de async iterators: o for

normal espera uma lista pronta, e o for await espera um valor de cada vez,

entrega o controle e volta quando o próximo chega.

É a forma curta de montar corpo, e serve melhor quando ele é grande e não

precisa caber inteiro na memória. Quando precisa caber — porque o JSON.parse

vai exigir o texto todo —, o caminho é Buffer.concat mesmo.

Exemplo

'use strict';

// Exemplo da aula 1 do dia 7: montar o corpo da requisicao.
//
// `req` nao vem com o corpo pronto. Ele e um STREAM: um pedaco de cada vez, e
// chega na ordem em que a rede entregou. Quem monta o corpo e o proprio
// servidor, ouvindo o evento `data` e juntando os pedacos.
//
// A aula mostra o caminho inteiro: `on('data')` -> `concat` -> `on('end')` ->
// `JSON.parse`, e o que acontece em cada degrau. E mostra os tres casos que
// quebram servidor: corpo vazio, corpo que nao e JSON, e corpo que promete mais
// do que manda (content-length mentiroso).

const http = require('node:http');

async function main() {
  // --- 1. o listener de corpo, a versao que serve para sempre ---
  //
  // `chunks` e um array de BUFFER, e cada `data` entrega um pedaco. O `+=` de
  // string faria a conversao a cada pedaco e perderia performance; o certo e
  // juntar os buffers e converter uma vez, no `end`.
  function lerCorpo(req) {
    return new Promise((resolve, reject) => {
      const chunks = [];
      let bytes = 0;
      req.on('data', (pedaco) => {
        chunks.push(pedaco);
        bytes += pedaco.length;
      });
      req.on('end', () => resolve({ bruto: Buffer.concat(chunks), bytes, pedacos: chunks.length }));
      req.on('error', reject);
    });
  }

  const servidor = http.createServer(async (req, res) => {
    const { bruto, bytes, pedacos } = await lerCorpo(req);
    const texto = bruto.toString('utf8');

    // O que chegou, em uma linha, e o que a aula precisa mostrar.
    if (req.url === '/inspecionar') {
      return res.end(JSON.stringify({
        metodo: req.method,
        content_type: req.headers['content-type'] || null,
        content_length: req.headers['content-length'] || null,
        bytes_recebidos: bytes,
        pedacos_recebidos: pedacos,
        corpo_bruto: texto,
      }, null, 1));
    }

    // O `content-length` declarado e o `bytes` contado: quando batem, o cliente
    // mandou tudo. Quando o cliente manda menos do que promete, o `end` chega
    // antes e o `content-length` e maior que o corpo real.
    if (req.url === '/json') {
      if (!texto) {
        res.writeHead(400, { 'Content-Type': 'application/json' });
        return res.end(JSON.stringify({ erro: 'corpo vazio' }));
      }
      try {
        const objeto = JSON.parse(texto);
        res.writeHead(200, { 'Content-Type': 'application/json' });
        return res.end(JSON.stringify({ recebido: objeto, campos: Object.keys(objeto) }));
      } catch (erro) {
        // O erro do `JSON.parse` tem posicao: e a unica pista de onde o texto
        // virou lixo, e ela diz o caracter exato.
        res.writeHead(400, { 'Content-Type': 'application/json' });
        return res.end(JSON.stringify({
          erro: 'JSON invalido',
          posicao: erro.message.match(/position (\d+)/)?.[1] || null,
          trecho: texto.slice(0, 40),
        }));
      }
    }

    res.writeHead(404);
    res.end('use /inspecionar ou /json');
  });

  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. um corpo pequeno, um pedaco so ---
    console.log('');
    console.log('--- POST /inspecionar, corpo pequeno ---');
    let r = await fetch(base + '/inspecionar', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ nm_aluno: 'Ana', turma: 3 }),
    });
    console.log(await r.text());

    // --- 2. um corpo que chega em VARIOS pedacos ---
    //
    // O `fetch` manda o corpo de uma vez para um corpo pequeno, e o Node junta
    // num `data` so. Para ver o stream de verdade, o exemplo monta um corpo
    // grande que o Node precisa quebrar em pedacos. O que muda na pratica e so
    // o numero de `data`: a montagem e a mesma.
    const grande = JSON.stringify({ nm_aluno: 'x'.repeat(300000) });
    console.log('');
    console.log('--- o MESMO corpo, agora com 300 KB ---');
    r = await fetch(base + '/inspecionar', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: grande,
    });
    const inspecionadoGrande = JSON.parse(await r.text());
    console.log('content-length declarado:', inspecionadoGrande.content_length, '(vem como TEXTO)');
    console.log('bytes recebidos:         ', inspecionadoGrande.bytes_recebidos, '(contado, numero)');
    console.log('pedacos recebidos:       ', inspecionadoGrande.pedacos_recebidos,
      '<-- varios: o Node entregou em pedacos, e o servidor juntou todos');
    console.log('os dois batem, depois de Number():',
      Number(inspecionadoGrande.content_length) === inspecionadoGrande.bytes_recebidos);
    console.log('o `content-length` e texto, e comparar direto com === daria false sempre;');
    console.log('e a pegadinha e o espelho da aula 1 do dia 6: header chega string.');

    // --- 3. corpo vazio ---
    //
    // `JSON.parse('')` LANCA. E o erro mais comum de API: rota que espera
    // objeto, cliente que mandou nada, e o `JSON.parse` explodindo com uma
    // mensagem que nao diz nada sobre o que o cliente fez de errado.
    console.log('');
    console.log('--- POST /json sem corpo nenhum ---');
    r = await fetch(base + '/json', { method: 'POST' });
    console.log('status:', r.status, '| corpo:', await r.text());
    console.log('');
    console.log('o mesmo JSON.parse com texto vazio, fora do servidor:');
    try {
      JSON.parse('');
    } catch (erro) {
      console.error(erro.name + ': ' + erro.message);
      console.log('  erro:', erro.name, '-', erro.message, ' <- SyntaxError, sem posicao util');
    }
    console.log('por isso a rota tem que testar o texto ANTES do parse.');

    // --- 4. corpo que nao e JSON ---
    console.log('');
    console.log('--- POST /json com texto que nao e JSON ---');
    r = await fetch(base + '/json', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: '{ nm_aluno: Ana, turma: 3 }',
    });
    console.log('status:', r.status, '| corpo:', await r.text());
    console.log('');
    console.log('o texto tem aspas duplas faltando: `nm_aluno: Ana` em vez de `"nm_aluno": "Ana"`');
    console.log('o `erro.message` do JSON.parse traz a posicao, e ela diz onde o parse parou.');

    // --- 5. o corpo chega CRU, sem o roteador adivinhar nada ---
    //
    // O servidor nao sabe se o corpo e JSON, formulario ou binario. Ele so sabe
    // o que o cabecalho `content-type` diz, e o parse e decisao de cada rota.
    console.log('');
    console.log('--- content-type e a unica pista do tipo ---');
    for (const [tipo, corpo] of [
      ['application/json', '{"a":1}'],
      ['text/plain', '{"a":1}'],
      ['text/plain', 'texto simples'],
    ]) {
      r = await fetch(base + '/inspecionar', {
        method: 'POST',
        headers: { 'Content-Type': tipo },
        body: corpo,
      });
      const visto = JSON.parse(await r.text());
      console.log('  content-type ' + tipo.padEnd(20) + ' corpo ' + JSON.stringify(corpo).padEnd(16) +
        ' -> mesmo texto recebido: ' + JSON.stringify(visto.corpo_bruto));
    }
    console.log('');
    console.log('os tres chegaram iguais. O parse e da rota, nao do servidor.');

    // --- 6. `for await` no corpo ---
    //
    // O `req` e um stream legivel, entao da para percorrer com `for await`.
    // A forma e mais curta, e serve melhor quando o corpo e grande e nao
    // precisa caber inteiro na memoria.
    const servidor2 = http.createServer(async (req, res) => {
      let total = 0;
      let pedacos = 0;
      for await (const pedaco of req) {
        total += pedaco.length;
        pedacos += 1;
      }
      res.end(JSON.stringify({ total, pedacos, metodo: 'for await' }));
    });
    await new Promise((resolve) => servidor2.listen(0, '127.0.0.1', resolve));
    const base2 = `http://127.0.0.1:${servidor2.address().port}`;
    const r2 = await fetch(base2 + '/', { method: 'POST', body: 'x'.repeat(300000) });
    const lido = JSON.parse(await r2.text());
    console.log('--- for await no corpo do stream ---');
    console.log(lido.total === 300000 && lido.pedacos > 1
      ? 'o `for await` contou os mesmos bytes em ' + lido.pedacos + ' pedacos'
      : 'o `for await` contou ' + lido.total + ' bytes em ' + lido.pedacos + ' pedacos');
    await new Promise((resolve) => servidor2.close(resolve));
    console.log('o segundo servidor foi encerrado no fim, com close().');
  } 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:37675
(a porta muda a cada execucao: o `listen(0)` pediu uma livre)

--- POST /inspecionar, corpo pequeno ---
{
 "metodo": "POST",
 "content_type": "application/json",
 "content_length": "28",
 "bytes_recebidos": 28,
 "pedacos_recebidos": 1,
 "corpo_bruto": "{\"nm_aluno\":\"Ana\",\"turma\":3}"
}

--- o MESMO corpo, agora com 300 KB ---
content-length declarado: 300015 (vem como TEXTO)
bytes recebidos:          300015 (contado, numero)
pedacos recebidos:        6 <-- varios: o Node entregou em pedacos, e o servidor juntou todos
os dois batem, depois de Number(): true
o `content-length` e texto, e comparar direto com === daria false sempre;
e a pegadinha e o espelho da aula 1 do dia 6: header chega string.

--- POST /json sem corpo nenhum ---
status: 400 | corpo: {"erro":"corpo vazio"}

o mesmo JSON.parse com texto vazio, fora do servidor:
  erro: SyntaxError - Unexpected end of JSON input  <- SyntaxError, sem posicao util
por isso a rota tem que testar o texto ANTES do parse.

--- POST /json com texto que nao e JSON ---
status: 400 | corpo: {"erro":"JSON invalido","posicao":"2","trecho":"{ nm_aluno: Ana, turma: 3 }"}

o texto tem aspas duplas faltando: `nm_aluno: Ana` em vez de `"nm_aluno": "Ana"`
o `erro.message` do JSON.parse traz a posicao, e ela diz onde o parse parou.

--- content-type e a unica pista do tipo ---
  content-type application/json     corpo "{\"a\":1}"      -> mesmo texto recebido: "{\"a\":1}"
  content-type text/plain           corpo "{\"a\":1}"      -> mesmo texto recebido: "{\"a\":1}"
  content-type text/plain           corpo "texto simples"  -> mesmo texto recebido: "texto simples"

os tres chegaram iguais. O parse e da rota, nao do servidor.
--- for await no corpo do stream ---
o `for await` contou os mesmos bytes em 6 pedacos
o segundo servidor foi encerrado no fim, com close().

servidor encerrado com close().
Aula 2

Validar o que chegou

Validar o que chegou

Validar não é "testar se o campo existe". É dizer, campo por campo: o que é

obrigatório, que tipo tem que ter, qual o tamanho, o que o cliente ganhou por

mandar lixo. E é aqui que a API decide se responde 400 ou 500 — a diferença

entre "o cliente mandou errado" e "o servidor quebrou".

A forma: lista de regras

const REGRAS = [
  { campo: 'nm_aluno', obrigatorio: true,
    teste: (v) => typeof v === 'string' && v.trim().length > 0,
    mensagem: 'nm_aluno e obrigatorio e tem que ser texto nao vazio' },
  { campo: 'turma', teste: (v) => Number.isInteger(v),
    mensagem: 'turma e obrigatorio e tem que ser numero inteiro' },
];

const erros = [];
for (const regra of REGRAS) {
  if (dados[regra.campo] === undefined && !regra.obrigatorio) continue;
  if (!regra.teste(dados[regra.campo])) erros.push(regra);
}

Cada regra declara o tipo esperado do campo (typeof v === 'string',

Number.isInteger(v)) e a mensagem que o cliente recebe quando ele não bate.

O tipo esperado vem antes do obrigatorio, e a ordem importa: um campo que não

veio reprova em qualquer tipo, e um campo que veio errado reprova só no tipo.

A função devolve todos os erros de uma vez, não só o primeiro. Isso é escolha

de API: quem errou três campos quer corrigir os três, não descobrir um por

requisição. O que ela entrega é um erro de validação por campo que reprovou, e

não um erro do servidor — a distinção é o que separa o status 400 do 500.

trim antes de validar: "veio" não é "tem conteúdo"

' ' passa em typeof === 'string' e reprova em .length > 0. A diferença

entre "o campo veio" e "o campo tem conteúdo" é a segunda que importa: um nome

de três espaços não é um nome.

O trim de entrada é o .trim(), e ele é aplicado ao valor que entra — antes

de comparar, para o teste não passar em dado que não tem nada dentro.

teste: (v) => typeof v === 'string' && v.trim().length > 0

Validar e sanear são dois passos

O trim acontece depois de validar, e o valor limpo é o que vai para o

banco. Validar responde "este dado é válido?"; sanear responde "como este dado

será gravado?". Fazer os dois de uma vez esconde uma decisão: o banco passa a

guardar texto normalizado sem que a validação tenha dito que normaliza.

A função que aplica essa limpeza é o que a documentação chama de sanitizar: ela

recebe o objeto já validado e devolve outro, com o valor gravável. O nome é de

higiene — é o passo que garante que o que foi validado é o que será salvo.

O corpo tem que ser objeto

JSON.parse aceita array, string e número. dados.nm_aluno em um array é

undefined — sem erro, só um 400 com mensagem de "campo obrigatório", que

aponta para o campo errado. O teste é typeof dados === 'object' && !Array.isArray(dados).

400 ou 422

CódigoRFCSignifica
4007231pedido malformado
4224918sintaxe ok, semântica errada

Os dois servem. O material usa 400 porque é o que a maioria dos frameworks e

dos clientes já espera, e trocar o código no meio do curso quebra o cliente que

já estava escrito.

A mensagem de erro também é API

"nm_aluno e obrigatorio e tem que ser texto nao vazio"   -> o cliente sabe o que fazer
"TypeError: Cannot read properties of undefined"           -> o cliente só trava

O formato é sempre o mesmo, e é ele que faz uma resposta de erro ser útil: o

status na linha de cabeçalho e o corpo com a lista do que reprovou, campo por

campo. Sem o corpo, o status 400 vira uma pista sem direção.

E o 500 é o caso oposto: quando a consulta ao banco estoura, não é 400, é

500 — e o detalhe técnico vai para o log, não para o corpo da resposta.

Um 500 que carrega SQL no corpo entrega o esquema do banco para quem deu o

erro.

Exemplo

'use strict';

// Exemplo da aula 2 do dia 7: validar o que chegou.
//
// Validar nao e "testar se o campo existe". E dizer, campo por campo: o que e
// obrigatorio, que tipo tem que ter, qual o tamanho, o que o cliente ganhou por
// mandar lixo. E o ponto em que a API decide se responde 400 ou 500 — e a
// diferenca entre "o cliente mandou errado" e "o servidor quebrou".
//
// O exemplo sobe um servidor, manda cinco pedidos malformados de proposito e
// mostra o status e o corpo de cada um. Cada resposta 400 diz o campo e o
// motivo, e e isso que o cliente precisa ver para corrigir.

const http = require('node:http');

async function main() {
  // --- 1. ler o corpo (aula 1 deste dia) ---
  function lerCorpo(req) {
    return new Promise((resolve, reject) => {
      const chunks = [];
      req.on('data', (p) => chunks.push(p));
      req.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
      req.on('error', reject);
    });
  }

  // --- 2. a funcao de validacao ---
  //
  // A forma: uma lista de REGRAS, cada uma com o nome do campo, o teste e a
  // mensagem. A funcao percorre e devolve TODOS os erros de uma vez — e nao so
  // o primeiro. Isso e escolha de API: quem errou tres campos quer corrigir os
  // tres, nao descobrir um por requisicao.
  const REGRAS = [
    {
      campo: 'nm_aluno',
      obrigatorio: true,
      teste: (v) => typeof v === 'string' && v.trim().length > 0,
      mensagem: 'nm_aluno e obrigatorio e tem que ser texto nao vazio',
    },
    {
      campo: 'nm_aluno',
      teste: (v) => v === undefined || v.trim().length <= 80,
      mensagem: 'nm_aluno pode ter no maximo 80 caracteres',
    },
    {
      campo: 'turma',
      obrigatorio: true,
      teste: (v) => Number.isInteger(v),
      mensagem: 'turma e obrigatorio e tem que ser numero inteiro',
    },
    {
      campo: 'turma',
      teste: (v) => v === undefined || (v >= 1 && v <= 12),
      mensagem: 'turma tem que estar entre 1 e 12',
    },
    {
      campo: 'email',
      teste: (v) => v === undefined || v === null || /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(String(v)),
      mensagem: 'email tem que ter o formato [email protected]',
    },
  ];

  function validar(dados) {
    const erros = [];
    for (const regra of REGRAS) {
      const valor = dados[regra.campo];
      // `undefined` em campo opcional: a regra nao obrigatoria e sempre passa.
      if (valor === undefined && !regra.obrigatorio) continue;
      if (!regra.teste(valor)) erros.push({ campo: regra.campo, mensagem: regra.mensagem });
    }
    return erros;
  }

  // --- 3. o `trim` antes de validar ---
  //
  // `'   '` passa em `typeof === 'string'` e reprova em `.length > 0`. A diferenca
  // entre "o campo veio" e "o campo tem conteudo", e e a segunda que importa: um
  // nome de tres espacios nao e um nome.
  console.log('--- trim antes de validar ---');
  for (const bruto of ['Ana', '   ', '', '  Carlos  ']) {
    console.log('  ' + JSON.stringify(bruto).padEnd(14) + ' -> trim: ' + JSON.stringify(bruto.trim()) +
      ' | length>0: ' + String(bruto.trim().length > 0).padEnd(5) +
      ' | sem trim: ' + String(bruto.length > 0));
  }
  console.log('  sem trim, "   " passa na validacao e grava tres espacios no banco.');

  // --- 4. o servidor ---
  const servidor = http.createServer(async (req, res) => {
    const texto = await lerCorpo(req);

    if (!texto) {
      res.writeHead(400, { 'Content-Type': 'application/json' });
      return res.end(JSON.stringify({ erro: 'corpo vazio' }));
    }
    let dados;
    try {
      dados = JSON.parse(texto);
    } catch (erro) {
      res.writeHead(400, { 'Content-Type': 'application/json' });
      return res.end(JSON.stringify({ erro: 'JSON invalido' }));
    }

    // O corpo precisa ser um objeto. Um array ou uma string passam pelo
    // `JSON.parse` e nao tem campo nenhum — e `dados.nm_aluno` seria `undefined`
    // em vez de dar erro.
    if (typeof dados !== 'object' || dados === null || Array.isArray(dados)) {
      res.writeHead(400, { 'Content-Type': 'application/json' });
      return res.end(JSON.stringify({ erro: 'o corpo tem que ser um objeto JSON' }));
    }

    const erros = validar(dados);
    if (erros.length > 0) {
      res.writeHead(400, { 'Content-Type': 'application/json' });
      return res.end(JSON.stringify({ erro: 'validacao falhou', quantidade: erros.length, campos: erros }));
    }

    // Sanear: o `trim` acontece DEPOIS de validar, e o valor limpo e o que vai
    // para o banco. Validar e sanar sao passos diferentes.
    const limpo = {
      nm_aluno: dados.nm_aluno.trim(),
      turma: dados.turma,
      email: dados.email ? String(dados.email).trim().toLowerCase() : null,
    };
    res.writeHead(201, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify({ id: 1, ...limpo, recebido_com_espaco: dados.nm_aluno !== limpo.nm_aluno }));
  });

  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('');
    console.log('servidor no ar em ' + base);
    console.log('(a porta muda a cada execucao: o `listen(0)` pediu uma livre)');

    // --- 5. os cinco pedidos, um por caso ---
    const casos = [
      ['valido', '{"nm_aluno":"Ana","turma":3}'],
      ['sem nm_aluno', '{"turma":3}'],
      ['nm_aluno so com espaco', '{"nm_aluno":"   ","turma":3}'],
      ['turma como texto', '{"nm_aluno":"Ana","turma":"3"}'],
      ['turma fora da faixa', '{"nm_aluno":"Ana","turma":99}'],
      ['email invalido', '{"nm_aluno":"Ana","turma":3,"email":"ana@"}'],
      ['erro em dois campos', '{"nm_aluno":"","turma":99}'],
      ['corpo que e array', '[{"nm_aluno":"Ana","turma":3}]'],
      ['corpo vazio', ''],
    ];

    for (const [nome, corpo] of casos) {
      const r = await fetch(base + '/alunos', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: corpo,
      });
      const resposta = await r.text();
      console.log('');
      console.log('--- ' + nome + ' -> ' + r.status + ' ---');
      console.log(resposta.length > 220 ? resposta.slice(0, 220) + '...' : resposta);
    }

    // --- 6. 400 e nao 422: por que o material usa 400 ---
    //
    // A RFC 7231 diz 400 para "pedido malformado" e a 2019 criou o 422 para
    // "sintaxe boa, semantica ruim". Os dois servem; o material usa 400 porque
    // e o que a maioria dos frameworks e de cliente ja espera, e trocar o
    // codigo no meio do curso quebra o cliente que ja estava escrito.
    console.log('');
    console.log('--- 400 ou 422 ---');
    console.log('400 (RFC 7231): pedido malformado — o que o material usa');
    console.log('422 (RFC 4918): sintaxe ok, semantica errada');
    console.log('os dois servem; 400 e o que os frameworks e os clientes ja esperam');

    // --- 7. o 500 e o caso oposto ---
    console.log('');
    console.log('--- 500: quando o erro NAO e do cliente ---');
    console.log('se a consulta ao banco estoura, nao e 400: e 500, e a mensagem');
    console.log('tecnica vai para o LOG, nao para o cliente. O 500 nunca carrega');
    console.log('detalhe de SQL: quem le o corpo da resposta pode ser o proprio atacante.');

    // --- 8. nunca devolver o erro cru ao cliente ---
    console.log('');
    console.log('--- a mensagem de erro tambem e API ---');
    console.log('"nm_aluno e obrigatorio e tem que ser texto nao vazio"  -> o cliente sabe o que fazer');
    console.log('"TypeError: Cannot read properties of undefined"          -> o cliente so trava');
  } 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

--- trim antes de validar ---
  "Ana"          -> trim: "Ana" | length>0: true  | sem trim: true
  "   "          -> trim: "" | length>0: false | sem trim: true
  ""             -> trim: "" | length>0: false | sem trim: false
  "  Carlos  "   -> trim: "Carlos" | length>0: true  | sem trim: true
  sem trim, "   " passa na validacao e grava tres espacios no banco.

servidor no ar em http://127.0.0.1:33123
(a porta muda a cada execucao: o `listen(0)` pediu uma livre)

--- valido -> 201 ---
{"id":1,"nm_aluno":"Ana","turma":3,"email":null,"recebido_com_espaco":false}

--- sem nm_aluno -> 400 ---
{"erro":"validacao falhou","quantidade":1,"campos":[{"campo":"nm_aluno","mensagem":"nm_aluno e obrigatorio e tem que ser texto nao vazio"}]}

--- nm_aluno so com espaco -> 400 ---
{"erro":"validacao falhou","quantidade":1,"campos":[{"campo":"nm_aluno","mensagem":"nm_aluno e obrigatorio e tem que ser texto nao vazio"}]}

--- turma como texto -> 400 ---
{"erro":"validacao falhou","quantidade":1,"campos":[{"campo":"turma","mensagem":"turma e obrigatorio e tem que ser numero inteiro"}]}

--- turma fora da faixa -> 400 ---
{"erro":"validacao falhou","quantidade":1,"campos":[{"campo":"turma","mensagem":"turma tem que estar entre 1 e 12"}]}

--- email invalido -> 400 ---
{"erro":"validacao falhou","quantidade":1,"campos":[{"campo":"email","mensagem":"email tem que ter o formato [email protected]"}]}

--- erro em dois campos -> 400 ---
{"erro":"validacao falhou","quantidade":2,"campos":[{"campo":"nm_aluno","mensagem":"nm_aluno e obrigatorio e tem que ser texto nao vazio"},{"campo":"turma","mensagem":"turma tem que estar entre 1 e 12"}]}

--- corpo que e array -> 400 ---
{"erro":"o corpo tem que ser um objeto JSON"}

--- corpo vazio -> 400 ---
{"erro":"corpo vazio"}

--- 400 ou 422 ---
400 (RFC 7231): pedido malformado — o que o material usa
422 (RFC 4918): sintaxe ok, semantica errada
os dois servem; 400 e o que os frameworks e os clientes ja esperam

--- 500: quando o erro NAO e do cliente ---
se a consulta ao banco estoura, nao e 400: e 500, e a mensagem
tecnica vai para o LOG, nao para o cliente. O 500 nunca carrega
detalhe de SQL: quem le o corpo da resposta pode ser o proprio atacante.

--- a mensagem de erro tambem e API ---
"nm_aluno e obrigatorio e tem que ser texto nao vazio"  -> o cliente sabe o que fazer
"TypeError: Cannot read properties of undefined"          -> o cliente so trava

servidor encerrado com close().