Competições Senac RS · Seletiva 26

Módulo 04 · Apostila teórica

Módulo 04 — Back-end e API RESTful

BaseUC13 (Desenvolver back-end web, 96h)Peso na prova20% (back-end web + banco)NaturezaRevisão
Ocupação: Desenvolvimento de Sistemas Treinador: Diogo Roehrs
💡Nota

Onde esse módulo entra. É o Módulo B da prova (2h30, 1º dia), a metade "servidor" do sistema. O banco (módulo 03) guarda os dados; a API é quem serve esses dados pro mundo e guarda as regras de negócio. Vale 20%, e sustenta o front (40%): um front lindo consumindo uma API quebrada não pontua. Natureza 🔄 revisão, as duas já fizeram API na Regional. O jogo agora é fazer uma API limpa, segura e correta que chegue no nível 3.

Como ler o código deste módulo: o conceito é sempre um só, mas onde a sintaxe muda de verdade o exemplo aparece nas duas stacks: Node.js + Express + MySQL e PHP + MySQL (PHP puro, sem framework). O SQL por baixo é o mesmo dos dois lados, é o banco do módulo 03. No treino a gente domina uma stack como principal e conhece a outra o bastante pra não travar se o cenário mudar.


1. O que é uma API e por que ela existe

API é a camada que fica entre o mundo (o front, o app, outro sistema) e o banco. Ninguém fala com o banco direto: fala com a API, e ela decide o que pode e o que não pode.

1.1 Estático vs dinâmico: os dois tipos de conteúdo web

O plano de curso (UC13) pede essa distinção, e ela explica a arquitetura inteira do sistema:

O sistema da prova junta os dois: um front estático (módulo 05) que busca dados dinâmicos na API. A separação é limpa: arquivos de um lado, dados do outro.

1.2 Agnóstica ao cliente: a fonte única da verdade

O descritivo diz uma frase que vale ouro: a API tem que ser agnóstica ao cliente. Quer dizer: não importa quem está consumindo (o front web, um app mobile, o Postman), a regra é a mesma, porque ela mora no servidor. A API é a fonte única da verdade.

🎯Foco de prova

🎯 A consequência prática disso. Regra de negócio (não deixar check-in duplo, não passar da capacidade, só admin pode apagar) vive na API, nunca só no front. O front pode até esconder um botão, mas quem impede de verdade é o back. Se a regra só existe no front, qualquer um que chame a API direto fura ela. Isso é conceito de mercado e é foco de prova: a banca testa chamando a API por fora do front.

1.3 Anatomia de uma requisição e de uma resposta

Toda conversa com a API tem o mesmo formato. A requisição:

POST /convidados HTTP/1.1
Host: localhost:3000
Content-Type: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

{ "nome": "Marina Souza", "casamento_id": 1, "acompanhantes": 2 }

Quatro partes: o método (POST), a rota (/convidados), os cabeçalhos (o Content-Type avisa que o corpo é JSON; o Authorization carrega o crachá da seção 8) e o corpo (o JSON com os dados).

A resposta espelha:

HTTP/1.1 201 Created
Content-Type: application/json

{ "id": 42, "nome": "Marina Souza", "casamento_id": 1, "acompanhantes": 2 }

Três partes: o status code (201, seção 2.3), os cabeçalhos e o corpo. Ler requisição e resposta com naturalidade é o que faz o Postman (seção 15) e a aba Network do navegador virarem ferramentas de debug em vez de mistério.


2. REST: o jeito organizado de montar a API

REST é um conjunto de convenções pra deixar a API previsível. A ideia central: você trata tudo como recursos (coisas), e age sobre eles com verbos padronizados. Quem conhece a convenção adivinha a rota sem ler documentação. A banca conhece a convenção.

2.1 Recursos e rotas: substantivos no plural

Cada tipo de coisa é um recurso com um endereço. As regras que o mercado consolidou:

⚠️Pega-ratão

⚠️ Pega-ratão: misturar convenções (/convidado, /Convidados, /lista_convidados no mesmo sistema). Cada rota individual funciona, mas o conjunto vira bagunça, e consistência é exatamente o tipo de coisa que o julgamento (J) do CIS enxerga. Decide o padrão no minuto 1 e não desvia.

2.2 Verbos HTTP: a ação certa pra cada coisa

Verbo O que faz Exemplo no Wedding Pass Idempotente?
GET Buscar/listar (não muda nada) GET /convidados Sim
POST Criar novo POST /convidados Não (cada chamada cria outro)
PUT Substituir o recurso inteiro PUT /convidados/12 manda todos os campos Sim
PATCH Alterar parte do recurso PATCH /convidados/12 manda só { "mesa_id": 3 } Na prática, sim
DELETE Apagar DELETE /convidados/12 Sim

Idempotente quer dizer: chamar duas vezes dá no mesmo que chamar uma. DELETE /convidados/12 duas vezes deixa o sistema no mesmo estado (o segundo dá 404, mas nada muda). POST duas vezes cria dois registros. Essa palavra impressiona na apresentação e explica por que o check-in duplicado (seção 11.2) é um problema real: dois POSTs criam dois check-ins, a menos que alguém impeça.

PUT vs PATCH na prova: os dois atualizam. PUT formalmente substitui tudo, PATCH altera campos. Escolhe um pros seus updates e usa sempre o mesmo; consistência vale mais que a sutileza teórica.

🎯Foco de prova

🎯 Foco de prova: o descritivo cita "usar os verbos HTTP corretamente" de forma explícita. Isso é avaliação objetiva. Usar POST pra buscar dado, ou GET pra apagar, é o tipo de erro que a banca marca na hora. Verbo certo pra ação certa é ponto garantido, e é só disciplina. Regra prática: GET nunca muda nada no banco.

2.3 Status codes: a tabela completa da prova

Toda resposta vem com um código dizendo o que aconteceu. A família conta a história: 2xx deu certo, 4xx o cliente errou, 5xx o servidor errou. Os que o Wedding Pass usa:

Código Nome Quando usar
200 OK GET que achou, PUT/PATCH que atualizou
201 Created POST que criou (convidado novo, check-in registrado)
204 No Content DELETE que deu certo (sem corpo na resposta)
400 Bad Request dado inválido: nome vazio, e-mail torto, acompanhantes negativo
401 Unauthorized não autenticado: sem token, token vencido ou inválido
403 Forbidden autenticado mas sem permissão: recepção tentando DELETE
404 Not Found o recurso não existe: GET /convidados/9999
409 Conflict conflito com o estado atual: check-in duplicado, e-mail já cadastrado, mesa cheia
500 Internal Server Error o servidor quebrou (bug não tratado; a seção 12 esconde o detalhe)

A dupla que mais confunde: 401 vs 403. 401 é "eu não sei quem você é" (falta crachá). 403 é "eu sei quem você é, e você não pode" (crachá de recepção na porta da sala de admin). A banca adora perguntar essa diferença, e o front (módulo 05) trata os dois diferente: 401 manda pro login, 403 mostra "sem permissão".

Existe também o 422 (dado bem formado mas inválido pela regra), que parte do mercado usa no lugar do 400. Na prova, 400 pra dado inválido resolve tudo; de novo, consistência vale mais que sofisticação.

⚠️Pega-ratão

⚠️ Pega-ratão: responder tudo com 200, até quando deu erro. Aí o front não sabe se foi sucesso ou falha e trata errado. Pior ainda: 200 com { "sucesso": false } dentro. Status code certo é o que deixa o front reagir direito (mostrar o erro, redirecionar, avisar). É pouco esforço pra bastante clareza, e é lido na avaliação.

2.4 O mapa de endpoints do Wedding Pass

Desenhar esse mapa antes de codar é o primeiro passo do Módulo B (e a Prova Surpresa cobra exatamente essa habilidade, módulo 07). O esqueleto do sistema:

Método + rota O que faz Quem pode
POST /auth/login Autentica e devolve o token público
GET /convidados Lista com busca, filtro e paginação (seção 13) logado
GET /convidados/:id Detalhe de um convidado logado
POST /convidados Cadastra convidado admin, organizador
PUT /convidados/:id Atualiza convidado admin, organizador
DELETE /convidados/:id Remove convidado admin
GET /casamentos/:id/capacidade Ocupação atual + alerta 90% logado
GET /mesas · POST /mesas · etc. CRUD de mesas admin, organizador
POST /convidados/:id/checkin Check-in com bloqueio de dupla entrada recepcao, admin
GET /convites/:codigo Consulta pública do convite (RSVP) público
POST /convites/:codigo/resposta Confirma ou recusa presença público

Os perfis (admin, organizador, recepcao) são os do ENUM da tabela usuario do módulo 03. O fluxo completo de convite e RSVP ganha vida no módulo 07; aqui o que importa é enxergar o sistema inteiro como uma tabela dessas.

🛠️Gambiarra boa

🛠️ Gambiarra boa: na prova, escreve esse mapa num papel ou num comentário no topo do projeto nos primeiros 10 minutos do Módulo B. Ele vira seu checklist de progresso (riscar rota pronta é motivador e evita esquecer endpoint) e sua resposta pronta quando a banca perguntar "como você organizou a API?".


3. Arquitetura em camadas: código organizado nas duas stacks

O plano de curso pede código orientado a objetos (UC4), padrões de projeto e MVC (UC9). Na prática de API, isso vira camadas: cada arquivo com um trabalho só.

3.1 As três camadas (e a regra de ouro)

A regra de ouro: cada camada só fala com a vizinha de baixo. Controller chama serviço, serviço chama repositório. SQL dentro do controller é a camada pulando o muro, e é o primeiro sintoma de código que vai virar nota 1 em organização.

🛠️Gambiarra boa

🛠️ Gambiarra boa: separar rende nota e velocidade. Parece burocracia, mas separar em camadas é o que deixa o código legível pra banca (ponto de julgamento) e fácil de você mesma achar o bug na hora do desespero: erro de SQL? Repositório. Regra errada? Serviço. Status errado? Controller. Quando tudo está num arquivo de 400 linhas, achar onde mexer custa minutos que você não tem.

3.2 O esqueleto em Node.js + Express

api/
├── server.js                  ← sobe o Express, registra middlewares e rotas
├── .env                       ← segredos (NUNCA vai pro Git; módulo 02, .gitignore)
├── package.json
└── src/
    ├── config/
    │   └── db.js              ← pool de conexão (seção 4.1)
    ├── routes/
    │   └── convidados.routes.js    ← só mapeia URL → controller
    ├── controllers/
    │   └── convidados.controller.js
    ├── services/
    │   └── convidados.service.js   ← regras de negócio
    ├── repositories/
    │   └── convidados.repository.js ← SQL, só SQL
    ├── middlewares/
    │   ├── auth.js            ← exigeLogin, exigePerfil (seções 8 e 9)
    │   └── erros.js           ← tratador central (seção 12)
    └── helpers/
        └── validar.js         ← o validador reutilizável (seção 5.1)

O server.js que amarra tudo:

require('dotenv').config();
const express = require('express');
const cors = require('cors');

const app = express();
app.use(cors());            // seção 10
app.use(express.json());    // sem isso, req.body chega undefined

app.use('/auth', require('./src/routes/auth.routes'));
app.use('/convidados', require('./src/routes/convidados.routes'));
app.use('/mesas', require('./src/routes/mesas.routes'));

app.use(require('./src/middlewares/erros'));  // por último, sempre

app.listen(3000, () => console.log('API no ar na porta 3000'));

E um arquivo de rotas, que é só o mapa:

// src/routes/convidados.routes.js
const router = require('express').Router();
const controller = require('../controllers/convidados.controller');
const { exigeLogin, exigePerfil } = require('../middlewares/auth');

router.get('/', exigeLogin, controller.listar);
router.get('/:id', exigeLogin, controller.buscar);
router.post('/', exigeLogin, exigePerfil('admin', 'organizador'), controller.criar);
router.put('/:id', exigeLogin, exigePerfil('admin', 'organizador'), controller.atualizar);
router.delete('/:id', exigeLogin, exigePerfil('admin'), controller.remover);
router.post('/:id/checkin', exigeLogin, exigePerfil('recepcao', 'admin'), controller.checkin);

module.exports = router;

Repara que esse arquivo é o mapa de endpoints da seção 2.4 em código. A banca lendo ele entende o sistema inteiro em 20 segundos. Isso é organização pontuando.

⚠️Pega-ratão

⚠️ Pega-ratão do express.json(): esquecer essa linha faz todo req.body chegar undefined e a validação recusar tudo. Sintoma clássico: "mando o JSON certo no Postman e a API diz que o campo é obrigatório". Confere o middleware antes de caçar bug na validação.

3.3 O esqueleto em PHP puro

Sem framework, o padrão é o front controller: toda requisição entra por um único index.php, que roteia.

api/
├── public/
│   └── index.php              ← único ponto de entrada (roteador)
├── composer.json              ← dependências (firebase/php-jwt)
├── .env                       ← segredos (NUNCA vai pro Git)
└── src/
    ├── config/conexao.php     ← PDO (seção 4.2)
    ├── controllers/
    ├── services/
    ├── repositories/
    └── helpers/
        ├── validar.php
        ├── auth.php           ← exigeLogin, exigePerfil
        └── resposta.php       ← json() e erro() padronizados

O roteador em public/index.php:

<?php
require __DIR__ . '/../vendor/autoload.php';
require __DIR__ . '/../src/helpers/resposta.php';
// ... demais requires

header('Content-Type: application/json; charset=utf-8');

$metodo = $_SERVER['REQUEST_METHOD'];
$uri    = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);

// "/convidados/12/checkin" → ['convidados', '12', 'checkin']
$partes  = explode('/', trim($uri, '/'));
$recurso = $partes[0] ?? '';
$id      = $partes[1] ?? null;
$acao    = $partes[2] ?? null;

$corpo = json_decode(file_get_contents('php://input'), true) ?? [];

try {
    match (true) {
        $metodo === 'POST'   && $recurso === 'auth' && $id === 'login' => login($corpo),
        $metodo === 'GET'    && $recurso === 'convidados' && !$id      => listarConvidados($_GET),
        $metodo === 'GET'    && $recurso === 'convidados'              => buscarConvidado($id),
        $metodo === 'POST'   && $recurso === 'convidados' && $acao === 'checkin' => fazerCheckin($id),
        $metodo === 'POST'   && $recurso === 'convidados'              => criarConvidado($corpo),
        $metodo === 'PUT'    && $recurso === 'convidados'              => atualizarConvidado($id, $corpo),
        $metodo === 'DELETE' && $recurso === 'convidados'              => removerConvidado($id),
        default => erro(404, 'Rota não encontrada'),
    };
} catch (Throwable $e) {
    error_log($e);                            // stack trace no log, não na resposta
    erro(500, 'Erro interno no servidor');    // seção 12
}

E os dois helpers de resposta que padronizam tudo:

<?php
// src/helpers/resposta.php
function json(int $status, array $dados): void {
    http_response_code($status);
    echo json_encode($dados, JSON_UNESCAPED_UNICODE);
    exit;
}

function erro(int $status, string $mensagem, array $detalhes = []): void {
    json($status, ['erro' => $mensagem, 'detalhes' => $detalhes]);
}
🛠️Gambiarra boa

🛠️ Gambiarra boa: subir a API PHP em um comando. php -S localhost:8000 index.php dentro da pasta public/ sobe um servidor de desenvolvimento que roteia tudo pelo index.php. Sem Apache, sem XAMPP, sem virtual host. Pra prova é perfeito; e o servidor embutido recarrega o código a cada requisição, então nem restart precisa.

3.4 Uma requisição atravessando as camadas

POST /convidados com o JSON da Marina, do começo ao fim:

  1. Rota casa POST /convidados e vê que exige login + perfil (middlewares passam ou barram com 401/403).
  2. Controller valida o formato do corpo (seção 5.1). Formato ruim: responde 400 e acabou. Formato ok: chama o serviço.
  3. Serviço aplica a regra: a mesa 3 tem lugar pra Marina + 2 acompanhantes? Não tem: lança erro 409. Tem: chama o repositório.
  4. Repositório roda o INSERT com prepared statement (seção 6) e devolve o id novo.
  5. A resposta volta o caminho: serviço → controller → 201 Created com o convidado criado no corpo.

Essa narração de fluxo, com um exemplo concreto, é exatamente o que o Módulo D (apresentação, módulo 10) pede. Treinar ela agora é estudar pra duas provas ao mesmo tempo.


4. Conexão com o banco: o cabo entre a API e o MySQL

Primeira coisa que o Módulo B precisa funcionando. As duas stacks, lado a lado.

4.1 Node: mysql2 com pool de conexões

// src/config/db.js
const mysql = require('mysql2/promise');

const pool = mysql.createPool({
  host: process.env.DB_HOST,
  user: process.env.DB_USER,
  password: process.env.DB_PASS,
  database: process.env.DB_NAME,
  waitForConnections: true,
  connectionLimit: 10,
});

module.exports = pool;

Por que pool e não uma conexão só: o pool mantém um conjunto de conexões abertas e empresta uma a cada consulta. Sem pool, ou você abre e fecha conexão a cada query (lento) ou segura uma conexão global que cai e derruba a API junto. createPool resolve os dois problemas com o mesmo esforço de digitação que createConnection. Não tem motivo pra não usar.

Usando (repare no destructuring do array que o mysql2 devolve):

const pool = require('../config/db');
const [linhas] = await pool.query('SELECT * FROM convidado WHERE casamento_id = ?', [1]);

4.2 PHP: PDO

PDO é a interface padrão do PHP pra banco. As três opções do construtor abaixo não são decoração, são o que faz o PDO se comportar direito:

<?php
// src/config/conexao.php
function conectar(): PDO {
    static $pdo = null;
    if ($pdo === null) {
        $pdo = new PDO(
            'mysql:host=localhost;dbname=wedding_pass;charset=utf8mb4',
            getenv('DB_USER'),
            getenv('DB_PASS'),
            [
                PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,  // erro vira exceção
                PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,        // fetch devolve array assoc.
                PDO::ATTR_EMULATE_PREPARES   => false,                   // prepared statement REAL
            ]
        );
    }
    return $pdo;
}

O static $pdo faz a função devolver sempre a mesma conexão dentro da requisição: o equivalente simples do pool pro modelo do PHP (que abre e fecha por requisição).

4.3 Configuração fora do código

Host, usuário, senha e nome do banco não podem estar chumbados no código, por dois motivos: trocar de máquina (a da prova!) sem editar código, e nunca commitar senha (módulo 02, .gitignore).

🛠️Gambiarra boa

🛠️ Gambiarra boa: commita um .env.exemplo com as chaves e valores falsos (DB_PASS=troque-aqui). Na máquina da prova, cp .env.exemplo .env, preenche e pronto. Você ganha o setup documentado de graça e a banca vê profissionalismo no repositório.


5. CRUD com validação: o feijão com arroz bem-feito

CRUD é Create, Read, Update, Delete: as quatro operações sobre cada recurso. O descritivo pede os CRUDs completos. Fazer CRUD é fácil; fazer CRUD validado é o que pontua.

Validar é não confiar no que chega. Antes de gravar: os obrigatórios vieram? Os formatos batem? As regras de negócio passam?

🎯Foco de prova

🎯 A validação é onde mora o nível 3. Um CRUD que grava qualquer coisa é nível 2. Um CRUD que recusa dado inválido com resposta clara (400 + a lista do que está errado) é nível 3. E lembra: a validação de verdade é no back. O front valida pra dar experiência boa (módulo 05); o back valida pra garantir.

5.1 O validador reutilizável (escreve uma vez, usa em todo endpoint)

Validar na mão em cada endpoint gera código repetido e esquecimento. O padrão de mercado é um validador genérico que recebe os dados e as regras:

Node:

// src/helpers/validar.js
function validar(dados, regras) {
  const erros = [];
  for (const [campo, r] of Object.entries(regras)) {
    const valor = dados?.[campo];
    const vazio = valor === undefined || valor === null || valor === '';

    if (r.obrigatorio && vazio) {
      erros.push(`${campo} é obrigatório`);
      continue;
    }
    if (vazio) continue;  // opcional não preenchido: ok

    if (r.max && String(valor).length > r.max)
      erros.push(`${campo} passa de ${r.max} caracteres`);
    if (r.regex && !r.regex.test(String(valor)))
      erros.push(`${campo} está num formato inválido`);
    if (r.inteiro && !Number.isInteger(Number(valor)))
      erros.push(`${campo} precisa ser um número inteiro`);
  }
  return erros;
}

module.exports = validar;

PHP:

<?php
// src/helpers/validar.php
function validar(array $dados, array $regras): array {
    $erros = [];
    foreach ($regras as $campo => $r) {
        $valor = $dados[$campo] ?? null;
        $vazio = $valor === null || $valor === '';

        if (($r['obrigatorio'] ?? false) && $vazio) {
            $erros[] = "$campo é obrigatório";
            continue;
        }
        if ($vazio) continue;

        if (isset($r['max']) && mb_strlen((string)$valor) > $r['max'])
            $erros[] = "$campo passa de {$r['max']} caracteres";
        if (isset($r['regex']) && !preg_match($r['regex'], (string)$valor))
            $erros[] = "$campo está num formato inválido";
        if (($r['inteiro'] ?? false) && filter_var($valor, FILTER_VALIDATE_INT) === false)
            $erros[] = "$campo precisa ser um número inteiro";
    }
    return $erros;
}

São 25 linhas que cobrem 90% da validação da prova inteira. O que muda por endpoint é só o conjunto de regras.

5.2 O POST /convidados completo em Node

As três camadas em ação. Controller:

// src/controllers/convidados.controller.js
const validar = require('../helpers/validar');
const service = require('../services/convidados.service');

const regrasConvidado = {
  nome:          { obrigatorio: true, max: 100 },
  email:         { regex: /^\S+@\S+\.\S+$/, max: 150 },
  telefone:      { max: 15 },
  acompanhantes: { inteiro: true },
  casamento_id:  { obrigatorio: true, inteiro: true },
};

async function criar(req, res, next) {
  try {
    const erros = validar(req.body, regrasConvidado);
    if (erros.length > 0) {
      return res.status(400).json({ erro: 'Dados inválidos', detalhes: erros });
    }
    const convidado = await service.criar(req.body);
    res.status(201).json(convidado);
  } catch (erro) {
    next(erro);  // manda pro tratador central (seção 12)
  }
}

Serviço (a regra de negócio que o controller não conhece):

// src/services/convidados.service.js
const repo = require('../repositories/convidados.repository');

async function criar(dados) {
  if (dados.mesa_id) {
    const livres = await repo.lugaresLivresNaMesa(dados.mesa_id);
    if (livres < 1 + (dados.acompanhantes ?? 0)) {
      const erro = new Error('A mesa não tem lugares suficientes');
      erro.status = 409;
      throw erro;
    }
  }
  return repo.inserir(dados);
}

Repositório (só SQL):

// src/repositories/convidados.repository.js
const pool = require('../config/db');

async function inserir(c) {
  const [resultado] = await pool.query(
    `INSERT INTO convidado (casamento_id, mesa_id, nome, email, telefone, acompanhantes)
     VALUES (?, ?, ?, ?, ?, ?)`,
    [c.casamento_id, c.mesa_id ?? null, c.nome, c.email ?? null,
     c.telefone ?? null, c.acompanhantes ?? 0]
  );
  return { id: resultado.insertId, ...c };
}

5.3 O mesmo endpoint em PHP

No PHP sem framework as camadas podem morar em funções, mas a separação é a mesma:

<?php
// src/controllers/convidados.controller.php
function criarConvidado(array $corpo): void {
    $erros = validar($corpo, [
        'nome'          => ['obrigatorio' => true, 'max' => 100],
        'email'         => ['regex' => '/^\S+@\S+\.\S+$/', 'max' => 150],
        'telefone'      => ['max' => 15],
        'acompanhantes' => ['inteiro' => true],
        'casamento_id'  => ['obrigatorio' => true, 'inteiro' => true],
    ]);
    if ($erros) erro(400, 'Dados inválidos', $erros);

    // regra de negócio (num projeto maior, iria pro service)
    if (!empty($corpo['mesa_id'])) {
        $livres = lugaresLivresNaMesa((int)$corpo['mesa_id']);
        if ($livres < 1 + (int)($corpo['acompanhantes'] ?? 0)) {
            erro(409, 'A mesa não tem lugares suficientes');
        }
    }

    $pdo = conectar();
    $stmt = $pdo->prepare(
        'INSERT INTO convidado (casamento_id, mesa_id, nome, email, telefone, acompanhantes)
         VALUES (?, ?, ?, ?, ?, ?)'
    );
    $stmt->execute([
        $corpo['casamento_id'],
        $corpo['mesa_id'] ?? null,
        $corpo['nome'],
        $corpo['email'] ?? null,
        $corpo['telefone'] ?? null,
        $corpo['acompanhantes'] ?? 0,
    ]);

    json(201, ['id' => (int)$pdo->lastInsertId()] + $corpo);
}

O resto do CRUD segue o mesmo molde. Dois detalhes que valem nota:

⚠️Pega-ratão

⚠️ Pega-ratão: validar só no front. É o mesmo erro da autorização: bonito pro usuário, furado pra quem chama a API direto. Validação no front melhora a experiência; validação no back protege os dados. A banca chama a API direto.


6. SQL injection: o ataque que a banca conhece pelo nome

Segurança web é conteúdo explícito da UC13, e injection é o item número 1 de toda lista de riscos (OWASP) há vinte anos. Entender o ataque é o que faz a defesa deixar de ser decoreba.

6.1 O ataque

Imagina um login montado por concatenação:

// ⚠️ NUNCA FAZER: a query montada colando strings
const sql = `SELECT * FROM usuario
             WHERE email = '${email}' AND senha = '${senha}'`;

Agora alguém digita no campo senha: ' OR '1'='1. A query final vira:

SELECT * FROM usuario
WHERE email = 'qualquer@coisa.com' AND senha = '' OR '1'='1';

'1'='1' é sempre verdadeiro, o OR engole o resto, a consulta devolve todos os usuários e o atacante entra sem senha. O dado do usuário virou código SQL. Esse é o problema inteiro em uma frase.

6.2 A defesa: prepared statements (que você já está usando)

Prepared statement separa a viagem: a query vai numa via, os dados vão na outra, e o MySQL nunca interpreta dado como SQL. O ' OR '1'='1 vira literalmente uma senha esquisita sendo procurada, e não acha nada.

Node (mysql2), o ? é o placeholder:

const [linhas] = await pool.query(
  'SELECT * FROM usuario WHERE email = ?',
  [email]
);

PHP (PDO), prepare + execute:

$stmt = $pdo->prepare('SELECT * FROM usuario WHERE email = ?');
$stmt->execute([$email]);
$usuario = $stmt->fetch();

A regra é binária e sem exceção: variável na query = placeholder. Todos os exemplos deste módulo já seguem isso; a seção existe pra você saber explicar o porquê (pergunta clássica de banca e material de apresentação).

6.3 Onde o placeholder não chega: ORDER BY e a whitelist

Placeholder funciona pra valores, não pra nomes de coluna ou direção de ordenação. ORDER BY ? não faz o que parece. Quando a ordenação vem do usuário (seção 13), a defesa é outra: whitelist.

const colunasPermitidas = ['nome', 'criado_em', 'acompanhantes'];
const ordem   = colunasPermitidas.includes(req.query.ordenar) ? req.query.ordenar : 'nome';
const direcao = req.query.direcao === 'desc' ? 'DESC' : 'ASC';

const sql = `SELECT * FROM convidado ORDER BY ${ordem} ${direcao}`;

A interpolação aqui é segura porque ordem e direcao só podem ser valores que você mesma escreveu. O usuário escolhe da sua lista, nunca injeta texto livre. Mesmo raciocínio em PHP com in_array().

🎯Foco de prova

🎯 Foco de prova: "por que você usou prepared statement?" é pergunta de banca com resposta pronta: "porque é a única defesa correta contra SQL injection: o dado viaja separado da query e nunca é interpretado como SQL". Trinta segundos, nível 3 de maturidade.


7. Senha no back: bcrypt e password_hash

O módulo 03 (seção 18.3) fechou o lado do banco: a coluna é senha_hash VARCHAR(255) e senha em texto puro nunca é gravada. Agora o lado da API: quem gera e quem confere o hash.

Node (bcryptjs):

const bcrypt = require('bcryptjs');

// no cadastro de usuário:
const hash = await bcrypt.hash(senha, 10);   // 10 = custo (rounds)

// no login:
const bate = await bcrypt.compare(senhaDigitada, usuario.senha_hash);

PHP (nativo, sem instalar nada):

// no cadastro:
$hash = password_hash($senha, PASSWORD_DEFAULT);   // bcrypt por padrão

// no login:
$bate = password_verify($senhaDigitada, $usuario['senha_hash']);

Por que existe compare/verify em vez de comparar strings: o bcrypt embute um sal aleatório em cada hash, então a mesma senha gera hashes diferentes a cada cadastro. Só a função sabe extrair o sal e refazer a conta. Consequências práticas:

🎯Foco de prova

🎯 Foco de prova (objetivo): senha com hash é item de checklist da banca. Abrir a tabela usuario e ver $2b$10$... em vez de 123456 é a diferença entre pontuar e zerar o critério. E saber explicar o sal é o upgrade de julgamento por cima do ponto objetivo.


8. Autenticação com JWT: o crachá do sistema

🎯Foco de prova

🎯 Foco de prova (objetivo): o descritivo pede autenticação com JWT. Ou está lá funcionando, ou é zero.

Autenticação responde quem é você (o login). Autorização (seção 9) responde o que você pode fazer. O JWT é a ponte entre as duas: o crachá que o login emite e que toda rota protegida confere.

8.1 A anatomia do token

Um JWT são três blocos de texto separados por ponto: header.payload.signature.

{
  "id": 3,
  "nome": "Ana",
  "perfil": "recepcao",
  "exp": 1767225600
}
⚠️Pega-ratão

⚠️ Pega-ratão que derruba gente experiente: o payload é apenas codificado em Base64, não criptografado. Qualquer um cola o token no jwt.io e lê o conteúdo. O que o token garante é que ninguém alterou (integridade), não que ninguém leu (sigilo). Logo: id, nome e perfil no payload, sim; senha ou qualquer dado sensível, jamais.

O campo exp é a expiração em timestamp. Token vencido = 401, mesmo com assinatura válida.

8.2 O login que emite o token

Node (jsonwebtoken):

// src/controllers/auth.controller.js
const jwt = require('jsonwebtoken');
const bcrypt = require('bcryptjs');
const pool = require('../config/db');

async function login(req, res, next) {
  try {
    const { email, senha } = req.body;
    const [linhas] = await pool.query('SELECT * FROM usuario WHERE email = ?', [email]);
    const usuario = linhas[0];

    if (!usuario || !(await bcrypt.compare(senha, usuario.senha_hash))) {
      return res.status(401).json({ erro: 'E-mail ou senha inválidos' });
    }

    const token = jwt.sign(
      { id: usuario.id, nome: usuario.nome, perfil: usuario.perfil },
      process.env.JWT_SECRET,
      { expiresIn: '8h' }
    );

    res.json({
      token,
      usuario: { id: usuario.id, nome: usuario.nome, perfil: usuario.perfil },
    });
  } catch (erro) { next(erro); }
}

PHP (firebase/php-jwt, via composer require firebase/php-jwt):

<?php
use Firebase\JWT\JWT;

function login(array $corpo): void {
    $pdo = conectar();
    $stmt = $pdo->prepare('SELECT * FROM usuario WHERE email = ?');
    $stmt->execute([$corpo['email'] ?? '']);
    $usuario = $stmt->fetch();

    if (!$usuario || !password_verify($corpo['senha'] ?? '', $usuario['senha_hash'])) {
        erro(401, 'E-mail ou senha inválidos');
    }

    $token = JWT::encode([
        'id'     => $usuario['id'],
        'nome'   => $usuario['nome'],
        'perfil' => $usuario['perfil'],
        'exp'    => time() + 60 * 60 * 8,   // 8 horas
    ], getenv('JWT_SECRET'), 'HS256');

    json(200, [
        'token'   => $token,
        'usuario' => ['id' => $usuario['id'], 'nome' => $usuario['nome'], 'perfil' => $usuario['perfil']],
    ]);
}

Dois detalhes de segurança embutidos aí:

8.3 O middleware que protege as rotas

Node:

// src/middlewares/auth.js
const jwt = require('jsonwebtoken');

function exigeLogin(req, res, next) {
  const cabecalho = req.headers.authorization ?? '';
  if (!cabecalho.startsWith('Bearer ')) {
    return res.status(401).json({ erro: 'Token não enviado' });
  }
  try {
    req.usuario = jwt.verify(cabecalho.slice(7), process.env.JWT_SECRET);
    next();
  } catch {
    return res.status(401).json({ erro: 'Token inválido ou expirado' });
  }
}

O jwt.verify confere assinatura e expiração de uma vez. Passou: os dados do crachá ficam em req.usuario pra qualquer camada usar (quem registrou o check-in? req.usuario.id). A rota usa o middleware como visto na seção 3.2.

PHP (sem middleware nativo, a proteção é a primeira linha do controller):

<?php
// src/helpers/auth.php
use Firebase\JWT\JWT;
use Firebase\JWT\Key;

function exigeLogin(): object {
    $cabecalho = $_SERVER['HTTP_AUTHORIZATION']
        ?? (function_exists('getallheaders') ? (getallheaders()['Authorization'] ?? '') : '');

    if (!str_starts_with($cabecalho, 'Bearer ')) {
        erro(401, 'Token não enviado');
    }
    try {
        return JWT::decode(substr($cabecalho, 7), new Key(getenv('JWT_SECRET'), 'HS256'));
    } catch (Exception $e) {
        erro(401, 'Token inválido ou expirado');
    }
}

E dentro de qualquer controller protegido:

function removerConvidado(?string $id): void {
    $usuario = exigeLogin();                        // barra com 401 se não passar
    exigePerfil($usuario, 'admin');                 // seção 9: barra com 403
    // ... daqui pra baixo, só quem pode
}
⚠️Pega-ratão

⚠️ Pega-ratão específico do PHP: o Apache às vezes descarta o header Authorization antes de chegar no PHP (aí $_SERVER['HTTP_AUTHORIZATION'] vem vazio e todo mundo toma 401 "sem motivo"). O fallback com getallheaders() acima resolve; com o servidor embutido (php -S) o problema nem existe. Se um dia a auth "parar do nada" no Apache, o suspeito é esse.

8.4 Expiração e onde o front guarda


9. Autorização por perfil (roles): o que cada crachá abre

O token carrega o perfil (o ENUM do módulo 03: admin, organizador, recepcao). Cada rota declara quais perfis aceita, e a API barra o resto com 403.

Node (o segundo middleware, parametrizado):

// src/middlewares/auth.js (continuação)
function exigePerfil(...perfis) {
  return (req, res, next) => {
    if (!perfis.includes(req.usuario.perfil)) {
      return res.status(403).json({ erro: 'Seu perfil não tem permissão pra essa ação' });
    }
    next();
  };
}

module.exports = { exigeLogin, exigePerfil };

Na rota, os dois em sequência (a ordem importa: primeiro sabe quem é, depois confere o que pode):

router.delete('/:id', exigeLogin, exigePerfil('admin'), controller.remover);
router.post('/:id/checkin', exigeLogin, exigePerfil('recepcao', 'admin'), controller.checkin);

PHP:

<?php
// src/helpers/auth.php (continuação)
function exigePerfil(object $usuario, string ...$perfis): void {
    if (!in_array($usuario->perfil, $perfis, true)) {
        erro(403, 'Seu perfil não tem permissão pra essa ação');
    }
}

A tabela de quem pode o quê já está pronta: é a coluna "Quem pode" do mapa da seção 2.4. Implementar autorização é transcrever aquela coluna pra middlewares.

🎯Foco de prova

🎯 Foco de prova: o sistema tem três perfis com poderes diferentes, e a autorização precisa estar no back. Testar isso é fácil pra banca: loga como recepção, copia o token, chama DELETE /convidados/12 no Postman. Se apagar, o controle de acesso inteiro vale zero, não importa quão bonito o front esconde o botão. O roteiro de teste da seção 15 inclui exatamente essa chamada.

⚠️ Pega-ratão: proteger as rotas "importantes" e esquecer as secundárias. GET /convidados aberto sem token vaza a lista inteira de convidados pra qualquer um. A regra: toda rota é protegida por padrão; pública é exceção declarada de propósito (login, página de RSVP).


10. CORS: deixar o front conversar com o back

Quando o front (por exemplo http://localhost:5500) chama a API (http://localhost:3000), o navegador bloqueia por segurança, a não ser que a API diga "pode, eu confio nessa origem". Isso é o CORS (Cross-Origin Resource Sharing). Detalhe que confunde: quem barra é o navegador do lado do front, mas quem autoriza é o servidor. Por isso a correção é sempre no back.

Tem ainda o preflight: antes de um POST com JSON ou com header Authorization, o navegador manda sozinho uma requisição OPTIONS perguntando "posso?". Se a API não responder o OPTIONS direito, o POST nem acontece, e o sintoma é o famoso "front não conecta".

Node: o pacote cors resolve tudo, inclusive o preflight:

const cors = require('cors');
app.use(cors());   // liberado geral: perfeito pra desenvolvimento e prova

PHP: os headers no topo do index.php, mais o tratamento do OPTIONS:

header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');

if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
    http_response_code(204);
    exit;   // preflight respondido, o navegador libera a requisição real
}
🎯Foco de prova

🎯 Foco de prova: o descritivo cita liberar CORS. É objetivo e é a causa número 1 de "meu front não conecta na API" no dia da prova. O erro aparece no console do navegador (em inglês, módulo 09: "blocked by CORS policy"), nunca no terminal da API, o que engana quem procura no lugar errado.

🛠️ Gambiarra boa: libera o CORS no minuto 1 do Módulo B, antes mesmo de existir front. São 2 linhas. Aí quando o front chegar no Módulo C, a ponte já está pronta e você não descobre o problema no pior momento. Em produção se restringiria a origem (origin: 'https://seusite.com'); na prova, * e bola pra frente, sabendo explicar por quê.


11. Regras de negócio no servidor: capacidade, corrida e transação

As regras que separam a API nota 2 da nota 3. Todas moram no serviço, todas têm o banco como última linha de defesa.

11.1 A regra de capacidade e o alerta de 90%

O casamento tem capacidade_total (tabela do módulo 03). A API precisa: impedir confirmação que estoure o limite e informar a ocupação pro dashboard acender o alerta de 90%.

A consulta que resolve os dois (uma subquery, módulo 03 seção 13):

SELECT
  ca.capacidade_total,
  (SELECT COALESCE(SUM(1 + cv.acompanhantes), 0)
     FROM convidado cv
     JOIN convite co ON co.convidado_id = cv.id
    WHERE cv.casamento_id = ca.id
      AND co.status = 'confirmado') AS confirmados
FROM casamento ca
WHERE ca.id = ?;

O serviço transforma em decisão:

async function statusCapacidade(casamentoId) {
  const { capacidade_total, confirmados } = await repo.capacidade(casamentoId);
  return {
    capacidade_total,
    confirmados,
    alerta90: confirmados >= capacidade_total * 0.9,
  };
}

async function confirmarPresenca(codigo) {
  const convite = await repo.buscarConvitePorCodigo(codigo);
  if (!convite) { const e = new Error('Convite não encontrado'); e.status = 404; throw e; }

  const { capacidade_total, confirmados } = await repo.capacidade(convite.casamento_id);
  const grupo = 1 + convite.acompanhantes;
  if (confirmados + grupo > capacidade_total) {
    const e = new Error('O casamento atingiu a capacidade máxima');
    e.status = 409; throw e;
  }
  return repo.confirmar(convite.id);
}

Repara: a conta inclui os acompanhantes (1 + acompanhantes por convidado). Esquecer os acompanhantes na soma é o clássico bug de regra de negócio que passa no teste rápido e explode na demo. E a conta mora no servidor porque é regra de negócio: dashboard, página de RSVP e Postman recebem o mesmo veredito.

11.2 O check-in duplo: condição de corrida e as três camadas de defesa

O descritivo pede bloquear dupla entrada: um convidado não entra duas vezes. A implementação ingênua:

1. SELECT: esse convidado já tem check-in?
2. Se não tem, INSERT.

Parece certa e tem um buraco: se dois pedidos chegam quase juntos (duas recepcionistas escaneiam o mesmo código, ou um clique duplo), os dois rodam o SELECT antes de qualquer INSERT, os dois leem "não tem", e os dois inserem. Isso é uma condição de corrida: o resultado depende da ordem de chegada em milissegundos que você não controla.

A defesa em três camadas, da cosmética à garantia:

  1. Front (módulo 05): desabilita o botão após o clique. Melhora a experiência, não garante nada (duas abas, dois computadores).
  2. API: não faz SELECT + INSERT; faz só o INSERT e trata o erro de duplicidade. Uma operação só, sem janela entre checar e agir.
  3. Banco: a constraint UNIQUE (convidado_id) na tabela checkin (módulo 03, seção 8). Essa é a garantia real. O MySQL é incapaz de aceitar o segundo INSERT, não importa quantos cheguem juntos.

O código da camada 2, que converte a recusa do banco em resposta educada:

Node:

// src/services/checkin.service.js
async function fazerCheckin(convidadoId, usuarioId) {
  try {
    const [r] = await pool.query(
      'INSERT INTO checkin (convidado_id, registrado_por) VALUES (?, ?)',
      [convidadoId, usuarioId]
    );
    return { id: r.insertId };
  } catch (erro) {
    if (erro.code === 'ER_DUP_ENTRY') {          // a UNIQUE barrou
      const jaEntrou = new Error('Esse convidado já fez check-in');
      jaEntrou.status = 409;
      throw jaEntrou;
    }
    throw erro;   // outro erro: deixa o tratador central cuidar
  }
}

PHP:

function fazerCheckin(?string $convidadoId): void {
    $usuario = exigeLogin();
    exigePerfil($usuario, 'recepcao', 'admin');

    try {
        $pdo = conectar();
        $stmt = $pdo->prepare('INSERT INTO checkin (convidado_id, registrado_por) VALUES (?, ?)');
        $stmt->execute([$convidadoId, $usuario->id]);
        json(201, ['mensagem' => 'Check-in registrado']);
    } catch (PDOException $e) {
        if (($e->errorInfo[1] ?? 0) === 1062) {   // 1062 = violação de UNIQUE no MySQL
            erro(409, 'Esse convidado já fez check-in');
        }
        throw $e;
    }
}

Variante útil quando não existe tabela própria (um campo de status na própria linha): o UPDATE atômico, que checa e age na mesma instrução:

UPDATE convite SET status = 'confirmado', respondido_em = NOW()
WHERE codigo = ? AND status = 'pendente';

Se affectedRows for 0, alguém confirmou antes (ou o código não existe): responde 409/404. Checagem e escrita numa operação só = sem janela de corrida.

🎯Foco de prova

🎯 Isso é assunto de nível 3 e de apresentação. A história pronta pro pitch do Módulo D: "o check-in tem três camadas de defesa: o botão desabilita no front, a API insere e trata a duplicidade, e a constraint UNIQUE no banco garante mesmo se dois pedidos chegarem juntos, porque existe condição de corrida entre checar e gravar". Quarenta segundos que mostram profundidade que a maioria não tem. Guarda essa pra contar no pitch.

11.3 Transações no código: tudo ou nada

O conceito veio do módulo 03 (seção 17); aqui é onde ele entra no código da API. Use quando duas ou mais escritas precisam acontecer juntas: confirmar o convite E atribuir a mesa; remanejar um convidado E liberar o lugar antigo.

Node: transação exige a mesma conexão do começo ao fim, então se pede uma emprestada ao pool:

const conexao = await pool.getConnection();
try {
  await conexao.beginTransaction();

  await conexao.query(
    "UPDATE convite SET status = 'confirmado', respondido_em = NOW() WHERE id = ?",
    [conviteId]
  );
  await conexao.query(
    'UPDATE convidado SET mesa_id = ? WHERE id = ?',
    [mesaId, convidadoId]
  );

  await conexao.commit();      // as duas valem
} catch (erro) {
  await conexao.rollback();    // nenhuma vale
  throw erro;
} finally {
  conexao.release();           // devolve a conexão pro pool, SEMPRE
}

PHP (PDO):

$pdo = conectar();
try {
    $pdo->beginTransaction();

    $pdo->prepare("UPDATE convite SET status = 'confirmado', respondido_em = NOW() WHERE id = ?")
        ->execute([$conviteId]);
    $pdo->prepare('UPDATE convidado SET mesa_id = ? WHERE id = ?')
        ->execute([$mesaId, $convidadoId]);

    $pdo->commit();
} catch (Exception $e) {
    $pdo->rollBack();
    throw $e;
}
⚠️Pega-ratão

⚠️ Pega-ratão do Node: rodar beginTransaction numa conexão e as queries em pool.query(). O pool pode entregar outra conexão pra cada query, e aí metade da "transação" roda fora dela. Transação no mysql2 = getConnection() + tudo pela mesma conexao + release() no finally. Esquecer o release vaza conexões até o pool secar e a API travar (10 requisições depois, do nada).


12. Tratamento de erro padronizado: um formato só pra tudo

Toda resposta de erro da API, de 400 a 500, no mesmo formato:

{ "erro": "mensagem legível pra quem usa", "detalhes": ["campo nome é obrigatório"] }

Por quê: o front (módulo 05) escreve um tratador pra exibir qualquer erro, a banca vê consistência, e você nunca perde tempo decidindo formato de novo. Os exemplos deste módulo já seguem esse formato; aqui é a central que garante ele até pros erros que você não previu.

Node: o middleware de erro (4 parâmetros, registrado por último no server.js):

// src/middlewares/erros.js
module.exports = (erro, req, res, next) => {
  const status = erro.status ?? 500;
  const mensagem = status === 500 ? 'Erro interno no servidor' : erro.message;

  if (status === 500) console.error(erro);   // stack trace no terminal, pra VOCÊ

  res.status(status).json({ erro: mensagem, detalhes: erro.detalhes ?? [] });
};

O padrão do time inteiro: serviço lança Error com .status; controller faz try/catch e chama next(erro); a central formata. Erro esperado (400, 404, 409) sai com a mensagem que o serviço escreveu; erro inesperado sai como 500 genérico.

PHP: o try/catch (Throwable) global do index.php (seção 3.3) já é a central. Erros esperados saem pelo helper erro() no meio do caminho; qualquer exceção não tratada vira error_log($e) + 500 genérico.

Por que o 500 é genérico de propósito: a mensagem crua de uma exceção pode vazar caminho de arquivo, SQL e estrutura interna. Isso é falha de segurança (informação pra atacante) e de acabamento (usuário vendo stack trace). O detalhe completo vai pro log/terminal, que é onde você debuga; pro cliente vai "Erro interno no servidor".

🎯Foco de prova

🎯 Foco de prova: mensagens de erro claras e consistentes são critério de julgamento, e o módulo 08 (lapidação) chama isso de "o ganho de nível 3 mais barato". A central de erro é o alicerce disso no back: 15 linhas, escritas uma vez, elevam TODAS as respostas de erro do sistema.


13. Paginação, filtro e ordenação no servidor

A UC13 cita ordenação e filtragem explicitamente, e o Wedding Pass tem a tela perfeita pra isso: a lista de convidados (com busca, filtro por mesa, ordenação e páginas). O contrato:

GET /convidados?busca=silva&mesa_id=3&ordenar=nome&direcao=asc&pagina=2&por_pagina=20

Tudo opcional, tudo com default sensato. A implementação (Node; em PHP muda $_GET e a sintaxe, a lógica é idêntica):

// src/repositories/convidados.repository.js
async function listar(filtros) {
  const clausulas = [];
  const valores = [];

  if (filtros.busca) {
    clausulas.push('nome LIKE ?');
    valores.push(`%${filtros.busca}%`);
  }
  if (filtros.mesa_id) {
    clausulas.push('mesa_id = ?');
    valores.push(Number(filtros.mesa_id));
  }

  const where = clausulas.length ? `WHERE ${clausulas.join(' AND ')}` : '';

  // whitelist (seção 6.3): nome de coluna NUNCA vem direto do usuário
  const ordem   = ['nome', 'criado_em', 'acompanhantes'].includes(filtros.ordenar)
                  ? filtros.ordenar : 'nome';
  const direcao = filtros.direcao === 'desc' ? 'DESC' : 'ASC';

  const porPagina = Math.min(Number(filtros.por_pagina) || 20, 100);  // teto anti-abuso
  const offset    = ((Number(filtros.pagina) || 1) - 1) * porPagina;

  const [linhas] = await pool.query(
    `SELECT * FROM convidado ${where} ORDER BY ${ordem} ${direcao} LIMIT ? OFFSET ?`,
    [...valores, porPagina, offset]
  );

  const [[{ total }]] = await pool.query(
    `SELECT COUNT(*) AS total FROM convidado ${where}`, valores
  );

  return { dados: linhas, total, pagina: Number(filtros.pagina) || 1, por_pagina: porPagina };
}

Os pontos que valem nota nesse bloco:

🛠️ Na prova, calibra o esforço: se a lista é pequena (mesas de um casamento), filtrar no front resolve e é honesto. Mas a lista de convidados é a lista grande do domínio, e o descritivo pede filtragem no servidor: implementa nela o pacote completo e aponta isso na apresentação. Um endpoint exemplar vale mais que cinco medianos.


14. ORM: o que é, e por que o SQL direto ganha a prova

A UC9 pede o conceito de ORM, então ele pode virar pergunta de banca mesmo que você nunca use um na prova.

ORM (Object-Relational Mapping) é a biblioteca que mapeia tabela em objeto e esconde o SQL: você escreve Convidado.findAll({ where: { mesa_id: 3 } }) e o ORM gera o SELECT. No mercado: Sequelize e Prisma no Node; Eloquent (Laravel) e Doctrine no PHP.

O que o ORM compra, e o que cobra:

ORM SQL direto (nosso caminho)
Produtividade em projeto grande alta (migrations, relações prontas) manual
Setup inicial modelos, config, docs da lib zero: a conexão da seção 4
Controle sobre a query você confia no que ele gera total: a query é a que você escreveu
Debug camada extra entre você e o erro o erro do MySQL na cara
Na prova de 2h30 tempo de setup que você não tem começa a pontuar no minuto 5

A decisão pra Seletiva é clara: SQL direto com prepared statements. Os motivos, prontos pra apresentação: o tempo de setup do ORM não se paga em 2h30; a banca avalia o SQL (módulo 03 vale 10% sozinho) e o SQL escondido no ORM não mostra domínio; e o debug transparente vale ouro sob pressão. A resposta nota 3 pra "por que não usou ORM?": "conheço o conceito, mapeia tabelas em objetos e agiliza projetos grandes, mas numa prova curta o SQL direto me dá controle, velocidade e mostra o domínio que está sendo avaliado".


15. Testar a API sem front: Postman, Insomnia e debug

O Módulo B termina antes de existir front. Como provar que a API funciona? Postman ou Insomnia (os dois servem; escolhe um e domina). O cronograma reserva tempo pra isso na semana da API, e a banca pode pedir pra ver.

Organização que rende: uma coleção por sistema, uma pasta por recurso (auth, convidados, mesas, check-in), uma requisição salva por endpoint. Duas variáveis de ambiente: {{base_url}} (troca localhost pela URL da prova em um lugar só) e {{token}} (cola o token do login uma vez, toda requisição usa Bearer {{token}} no header Authorization).

O roteiro de teste de cada endpoint protegido (4 chamadas, sempre as mesmas):

  1. Sem token → tem que dar 401.
  2. Com token de perfil errado (recepção tentando DELETE) → tem que dar 403.
  3. Com dado inválido (nome vazio) → tem que dar 400 com a lista de detalhes.
  4. Com tudo certo → 200/201 e o dado confere no banco.

Mais os especiais: check-in duas vezes no mesmo convidado (a segunda tem que dar 409) e GET de id inexistente (404). Se a coleção inteira passa nesse roteiro, a API está defendida, e essa frase é literalmente o fechamento do Módulo B.

Quando dá errado, o kit de debug:


16. Checklist de segurança do Módulo B

Os riscos do OWASP Top 10 filtrados pro que a banca enxerga e testa, cada um com defesa já construída neste módulo:

Risco (OWASP) Como apareceria na prova Defesa Seção
Injection login furado com ' OR '1'='1 prepared statements sempre; whitelist no ORDER BY 6
Falha de autenticação senha em texto puro; token sem expiração bcrypt/password_hash; JWT com exp e segredo no .env 7, 8
Falha de controle de acesso endpoint aberto; perfil não conferido exigeLogin + exigePerfil em toda rota não pública 9
Exposição de dados stack trace na resposta; senha_hash no JSON 500 genérico; nunca devolver a coluna de senha 12

O último item merece o reforço: SELECT * na tabela usuario devolve o senha_hash junto, e ele vaza pro front no JSON do login ou da listagem. Lista as colunas (SELECT id, nome, email, perfil FROM usuario) ou remove o campo antes do res.json. É o tipo de vazamento que a banca acha em 10 segundos abrindo a aba Network.

A checklist de fim de Módulo B, pra rodar nos últimos 10 minutos:


17. Autoavaliação do módulo

  1. O que quer dizer a API ser "agnóstica ao cliente", e por que a regra de negócio mora nela?
  2. Qual a diferença entre conteúdo estático e dinâmico, e onde cada um vive no Wedding Pass?
  3. Qual verbo HTTP pra cada ação (criar, buscar, atualizar, apagar)? O que é idempotência?
  4. Qual a diferença entre 401 e 403? E quando usar 409?
  5. O que faz cada camada (controller, serviço, repositório) e qual a regra de ouro entre elas?
  6. Por que usar pool de conexões no Node? E o que fazem as três opções do PDO na conexão?
  7. Como funciona o ataque ' OR '1'='1 e por que o prepared statement barra ele?
  8. Por que ORDER BY ? não funciona e qual a defesa certa pra ordenação vinda do usuário?
  9. Por que existe bcrypt.compare/password_verify em vez de comparar os hashes com ===?
  10. Quais as três partes de um JWT e o que cada uma garante? O que NUNCA vai no payload, e por quê?
  11. Qual a diferença entre autenticação e autorização, e onde cada middleware entra?
  12. O que é o preflight (OPTIONS) do CORS e qual o sintoma clássico quando ele não é tratado?
  13. O que é a condição de corrida no check-in e quais as três camadas de defesa?
  14. Quando usar transação na API, e por que no Node ela exige getConnection() em vez de pool.query()?
  15. Por que a resposta de erro 500 tem que ser genérica, e pra onde vai o detalhe do erro?
  16. Se a banca perguntar "por que você não usou ORM?", qual a resposta nota 3?

Referências do módulo

Bibliografia do plano de curso (UC13/UC9):

Segurança:

Referências de mercado (pesquisa web):


💡Nota

Back-end no lugar? Agora vem o módulo mais pesado da prova, onde tudo isso vira tela: 05 — Front-end web.

Seletiva 26 · Desenvolvimento de Sistemas · Senac RS