04-CRUD Completo
Materiais desta aula
04 — CRUD Completo
Sumário
- A arquitetura completa
- Create
- Read
- Update
- Delete
- Validando a entrada
- Tratando erros: 404 e erro central
- Juntando tudo: as rotas
- Status codes
1. A arquitetura completa
O mesmo padrão de Back-end I, agora com SQL de verdade e uma peça nova — a validação vira um middleware próprio, que roda antes do controller:
routes → define a URL e o verbo HTTP
validação → confere os dados antes de agir (novo!)
controller → monta a resposta
service → conversa com o banco (SQL)
Se os dados estiverem errados, a requisição nem chega no controller — o middleware de validação já responde com 400.
Nos controllers, o service inteiro é importado com um apelido curto, para deixar o código mais enxuto:
import * as service from '../services/produtosService.js';
2. Create
services/produtosService.js
export async function criar(produto) {
const { nome, preco, categoria_id } = produto;
const [r] = await pool.query(
'INSERT INTO produtos (nome, preco, categoria_id) ' +
'VALUES (?, ?, ?)',
[nome, preco, categoria_id]
);
return r.insertId;
}
> Os ? no lugar de cada valor evitam SQL Injection — nunca concatene valores direto na string da query.
controllers/produtosController.js
export async function criar(req, res, next) {
try {
const id = await service.criar(req.body);
res.status(201).json({ id, ...req.body });
} catch (erro) {
next(erro);
}
}
Repare que não há validação aqui — isso já foi resolvido antes, pelo middleware validarProduto (seção 6). E o catch não responde 500 diretamente: ele chama next(erro), passando a bola para o error handler central (seção 7).
3. Read
Listar todos
// service
export async function listarTodos() {
const [rows] = await pool.query('SELECT * FROM produtos');
return rows;
}
// controller
export async function listar(req, res, next) {
try {
const produtos = await service.listarTodos();
res.json(produtos);
} catch (erro) {
next(erro);
}
}
Buscar por id
// service
export async function buscarPorId(id) {
const [rows] = await pool.query(
'SELECT * FROM produtos WHERE id = ?', [id]
);
return rows[0];
}
// controller
export async function buscarPorId(req, res, next) {
try {
const { id } = req.params;
const produto = await service.buscarPorId(id);
if (!produto) {
return res.status(404).json({ erro: 'sem registro' });
}
res.json(produto);
} catch (erro) {
next(erro);
}
}
4. Update
// service
export async function atualizar(id, produto) {
const { nome, preco, categoria_id } = produto;
const [r] = await pool.query(
'UPDATE produtos SET nome=?, preco=?, ' +
'categoria_id=? WHERE id=?',
[nome, preco, categoria_id, id]
);
return r.affectedRows;
}
// controller
export async function atualizar(req, res, next) {
try {
const { id } = req.params;
const existente = await service.buscarPorId(id);
if (!existente) {
return res.status(404).json({ erro: 'sem registro' });
}
await service.atualizar(id, req.body);
res.json({ id, ...req.body });
} catch (erro) {
next(erro);
}
}
> Por que checar buscarPorId antes, em vez de olhar affectedRows? Se o UPDATE não muda nenhum valor (os dados enviados são iguais aos já salvos), o MySQL retorna affectedRows = 0 mesmo com o registro existindo — isso daria um 404 incorreto. Buscando o registro primeiro, o 404 só acontece quando ele realmente não existe.
5. Delete
// service
export async function deletar(id) {
const [r] = await pool.query(
'DELETE FROM produtos WHERE id = ?', [id]
);
return r.affectedRows;
}
// controller
export async function deletar(req, res, next) {
try {
const { id } = req.params;
const n = await service.deletar(id);
if (n === 0) {
return res.status(404).json({ erro: 'sem registro' });
}
res.status(204).send();
} catch (erro) {
next(erro);
}
}
> Aqui o affectedRows continua confiável: um DELETE ou apaga uma linha, ou não apaga nenhuma — sem meio-termo ambíguo, ao contrário do UPDATE.
6. Validando a entrada
Um middleware roda antes do controller e decide se a requisição continua (next()) ou é interrompida com um erro:
// middlewares/validarProduto.js
export function validarProduto(req, res, next) {
const { nome, preco } = req.body;
const erros = [];
if (!nome) erros.push('nome e obrigatorio');
if (!preco || preco <= 0) erros.push('preco invalido');
if (erros.length > 0) {
return res.status(400).json({ erros });
}
next();
}
No projeto completo, vale validar também quantidade_estoque e categoria_id — o princípio é o mesmo: checar tipo e presença de cada campo, acumular mensagens de erro, e só chamar next() se estiver tudo certo.
7. Tratando erros: 404 e erro central
Diferente da validação, essas duas peças não ganham arquivo próprio — ficam definidas direto no index.js, depois de todas as rotas, do mesmo jeito que vocês já fizeram em Back-end I:
// index.js, depois de app.use('/api', produtosRoutes)
app.use((req, res) => {
res.status(404).json({ erro: 'rota nao encontrada' });
});
app.use((erro, req, res, next) => {
console.error(erro);
res.status(500).json({ erro: 'erro interno' });
});
- A primeira função só é alcançada se nenhuma rota cadastrada bateu com a requisição — por isso vem depois de todas as rotas.
- A segunda função vira um error handler porque tem exatamente 4 parâmetros (
erro, req, res, next) — é assim que o Express reconhece que essa função trata erros, e não é mais uma rota normal. Sempre que um controller chamanext(erro), a execução pula direto para cá.
> Por que validarProduto é arquivo separado e essas duas não são? Porque validarProduto precisa ser reaproveitado em mais de uma rota (POST e PUT) — vale a pena isolar. Já o 404 e o error handler são usados uma única vez cada, no fim do index.js; criar um arquivo só para isso seria mais uma camada sem ganho real.
8. Juntando tudo: as rotas
// routes/produtosRoutes.js
import express from 'express';
import * as controller from '../controllers/produtosController.js';
import { validarProduto } from '../middlewares/validarProduto.js';
const router = express.Router();
router.post('/produtos', validarProduto, controller.criar);
router.get('/produtos', controller.listar);
router.get('/produtos/:id', controller.buscarPorId);
router.put('/produtos/:id', validarProduto, controller.atualizar);
router.delete('/produtos/:id', controller.deletar);
export default router;
// index.js
app.use('/api', produtosRoutes);
// a partir daqui, só entra quem não encontrou uma rota válida acima —
// e por último, o error handler, que só é alcançado via next(erro)
app.use((req, res) => { /* 404 — código completo na seção 7 */ });
app.use((erro, req, res, next) => { /* erro central — código completo na seção 7 */ });
O middleware de validação entra entre a rota e o controller — só nas rotas que recebem dados (POST e PUT).
9. Status codes
| Código | Nome | Quando usar |
|---|---|---|
| 200 | OK | Sucesso — leitura ou atualização |
| 201 | Created | Recurso criado com sucesso |
| 204 | No Content | Sucesso, sem corpo de resposta (delete) |
| 400 | Bad Request | Dados inválidos ou faltando (pego pelo middleware de validação) |
| 404 | Not Found | Registro não encontrado, ou rota inexistente |
| 500 | Server Error | Erro inesperado — sempre tratado pelo erro central do index.js |