Express + MongoDB + Auth + Validação + Error Handling.
api-produtos/
├── controllers/
│ ├── authController.js
│ └── produtoController.js
├── middleware/
│ ├── auth.js
│ └── errorHandler.js
├── models/
│ ├── Usuario.js
│ └── Produto.js
├── routes/
│ ├── authRoutes.js
│ └── produtoRoutes.js
├── .env
├── .gitignore
├── package.json
└── index.js
$ mkdir api-produtos && cd api-produtos
$ npm init -y
$ npm install express mongoose dotenv \
bcryptjs jsonwebtoken cors
$ npm install -D nodemon
// package.json scripts
{
"scripts": {
"dev": "nodemon index.js",
"start": "node index.js"
}
}
require('dotenv').config();
const express = require('express');
const mongoose = require('mongoose');
const cors = require('cors');
const errorHandler = require('./middleware/errorHandler');
const app = express();
// Middlewares globais
app.use(cors());
app.use(express.json());
// Rotas
app.use('/api/auth', require('./routes/authRoutes'));
app.use('/api/produtos', require('./routes/produtoRoutes'));
// Middleware de erro (sempre por último)
app.use(errorHandler);
// Conexão + Start
mongoose.connect(process.env.MONGODB_URI)
.then(() => {
console.log('✅ MongoDB conectado');
app.listen(process.env.PORT || 3000, () =>
console.log('🚀 Servidor rodando')
);
});
const mongoose = require('mongoose');
const produtoSchema = new mongoose.Schema({
nome: { type: String, required: [true, 'Nome obrigatório'], trim: true },
descricao: { type: String, default: '' },
preco: { type: Number, required: true, min: [0, 'Preço não pode ser negativo'] },
categoria: { type: String, required: true,
enum: ['Eletrônicos', 'Roupas', 'Alimentos', 'Livros', 'Outros']
},
estoque: { type: Number, default: 0, min: 0 },
ativo: { type: Boolean, default: true },
criadoPor: { type: mongoose.Schema.Types.ObjectId, ref: 'Usuario' }
}, { timestamps: true });
module.exports = mongoose.model('Produto', produtoSchema);
enum limita os valores aceitos. ref cria um relacionamento com o model Usuario.
const Produto = require('../models/Produto');
exports.listar = async (req, res, next) => {
try {
const { categoria, ordem, pagina = 1, limite = 10 } = req.query;
const filtro = { ativo: true };
if (categoria) filtro.categoria = categoria;
const produtos = await Produto.find(filtro)
.sort(ordem === 'preco' ? { preco: 1 } : { createdAt: -1 })
.skip((pagina - 1) * limite)
.limit(Number(limite));
const total = await Produto.countDocuments(filtro);
res.json({ produtos, total, pagina: Number(pagina), paginas: Math.ceil(total / limite) });
} catch (err) { next(err); }
};
exports.buscar = async (req, res, next) => {
try {
const produto = await Produto.findById(req.params.id);
if (!produto) return res.status(404).json({ erro: 'Produto não encontrado' });
res.json(produto);
} catch (err) { next(err); }
};
exports.criar = async (req, res, next) => {
try {
req.body.criadoPor = req.userId;
const produto = await Produto.create(req.body);
res.status(201).json(produto);
} catch (err) { next(err); }
};
exports.atualizar = async (req, res, next) => {
try {
const produto = await Produto.findByIdAndUpdate(
req.params.id,
req.body,
{ new: true, runValidators: true }
);
if (!produto) return res.status(404).json({ erro: 'Não encontrado' });
res.json(produto);
} catch (err) { next(err); }
};
exports.remover = async (req, res, next) => {
try {
const produto = await Produto.findByIdAndDelete(req.params.id);
if (!produto) return res.status(404).json({ erro: 'Não encontrado' });
res.status(204).send();
} catch (err) { next(err); }
};
runValidators: true garante que o update respeite as validações do schema. new: true retorna o doc atualizado.
const router = require('express').Router();
const ctrl = require('../controllers/produtoController');
const auth = require('../middleware/auth');
// Públicas (leitura)
router.get('/', ctrl.listar);
router.get('/:id', ctrl.buscar);
// Protegidas (escrita)
router.post('/', auth, ctrl.criar);
router.put('/:id', auth, ctrl.atualizar);
router.delete('/:id', auth, ctrl.remover);
module.exports = router;
🔓 pública 🔒 requer token
module.exports = (err, req, res, next) => {
console.error('❌', err.message);
// Erro de validação do Mongoose
if (err.name === 'ValidationError') {
const erros = Object.values(err.errors)
.map(e => e.message);
return res.status(400).json({ erro: 'Validação', detalhes: erros });
}
// ID inválido do MongoDB
if (err.name === 'CastError')
return res.status(400).json({ erro: 'ID inválido' });
// Duplicata (unique)
if (err.code === 11000)
return res.status(409).json({ erro: 'Registro duplicado' });
res.status(500).json({ erro: 'Erro interno do servidor' });
};
err, req, res, next) é reconhecido pelo Express como error handler.
GET /api/produtos?categoria=Eletrônicos
Filtra por categoria
GET /api/produtos?ordem=preco
Ordena por preço
GET /api/produtos?pagina=2&limite=5
Página 2 com 5 itens
{
"produtos": [...],
"total": 47,
"pagina": 2,
"paginas": 10
}
total e paginas permite ao front-end montar a navegação de páginas.
# 1. Registrar
curl -X POST http://localhost:3000/api/auth/registrar \
-H "Content-Type: application/json" \
-d '{"nome":"Ana","email":"ana@email.com","senha":"123456"}'
# 2. Login → copiar o token
curl -X POST http://localhost:3000/api/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"ana@email.com","senha":"123456"}'
# 3. Criar produto (com token)
curl -X POST http://localhost:3000/api/produtos \
-H "Content-Type: application/json" \
-H "Authorization: Bearer SEU_TOKEN" \
-d '{"nome":"Notebook","preco":2999.90,"categoria":"Eletrônicos"}'
# 4. Listar (sem token)
curl http://localhost:3000/api/produtos
.env com sua connection string do Atlasenum e ref$regex) e ordenação por múltiplos campos.
Front-end consumindo API própria.