Até agora nossa API guardava dados em um array na memória — tudo sumia quando o servidor reiniciava. Uma aplicação real precisa de um banco de dados persistente.
O MongoDB é um banco de dados NoSQL orientado a documentos — em vez de tabelas e linhas como no SQL, ele armazena coleções de documentos JSON. É a escolha mais comum no ecossistema Node.js pela naturalidade com JavaScript.
O Mongoose é a biblioteca que conecta o Node ao MongoDB, adicionando schemas, validações, e uma API elegante sobre o driver nativo.
SQL vs NoSQL — quando usar cada um
┌─────────────────────────────────────────────────────────┐
│ SQL (PostgreSQL, MySQL) │
│ │
│ Dados: Tabelas com colunas fixas │
│ Relações: JOINs entre tabelas │
│ Schema: Rígido — definido antes dos dados │
│ Consultas: SQL padronizado │
│ Ideal para: finanças, ERP, dados altamente relacionais │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ NoSQL — MongoDB │
│ │
│ Dados: Coleções de documentos JSON │
│ Relações: Referências ou embedding │
│ Schema: Flexível — documentos podem variar │
│ Consultas: API própria (find, aggregate...) │
│ Ideal para: catálogos, redes sociais, IoT, conteúdo │
└─────────────────────────────────────────────────────────┘
Instalando e conectando
# Instalar Mongoose
npm install mongoose
# Para desenvolvimento local, você pode:
# 1. Instalar MongoDB localmente: https://www.mongodb.com/try/download/community
# 2. Usar MongoDB Atlas (gratuito): https://www.mongodb.com/atlas
# 3. Usar Docker: docker run -d -p 27017:27017 mongo
// src/config/database.js
const mongoose = require("mongoose");
async function conectar() {
const url = process.env.MONGODB_URL || "mongodb://localhost:27017/meuapp";
try {
await mongoose.connect(url, {
// Opções recomendadas
serverSelectionTimeoutMS: 5000, // timeout de 5s para encontrar servidor
socketTimeoutMS: 45000, // timeout de operações
});
console.log(`✅ MongoDB conectado: ${mongoose.connection.host}`);
// Eventos de conexão
mongoose.connection.on("disconnected", () => {
console.warn("⚠️ MongoDB desconectado.");
});
mongoose.connection.on("error", (erro) => {
console.error("❌ Erro no MongoDB:", erro.message);
});
} catch (erro) {
console.error("❌ Falha ao conectar ao MongoDB:", erro.message);
process.exit(1); // Encerra se não conseguir conectar
}
}
async function desconectar() {
await mongoose.connection.close();
console.log("MongoDB desconectado.");
}
module.exports = { conectar, desconectar };
// src/index.js
require("dotenv").config();
const express = require("express");
const { conectar } = require("./config/database");
const app = express();
app.use(express.json());
// Conecta ao banco ANTES de iniciar o servidor
conectar().then(() => {
app.listen(3000, () => {
console.log("🚀 Servidor rodando em http://localhost:3000");
});
});
Schemas e Models — definindo a estrutura dos dados
No Mongoose, um Schema define a estrutura de um documento. Um Model é a classe que usamos para interagir com a coleção:
// src/models/Usuario.js
const mongoose = require("mongoose");
const { Schema } = mongoose;
const usuarioSchema = new Schema(
{
// Tipos básicos
nome: {
type: String,
required: [true, "Nome é obrigatório."],
trim: true,
minlength: [2, "Nome deve ter pelo menos 2 caracteres."],
maxlength: [100, "Nome não pode exceder 100 caracteres."],
},
email: {
type: String,
required: [true, "Email é obrigatório."],
unique: true, // cria índice único
lowercase: true, // converte para minúsculo automaticamente
trim: true,
match: [/^S+@S+.S+$/, "Email inválido."],
},
senha: {
type: String,
required: true,
minlength: 6,
select: false, // nunca retorna a senha em queries
},
idade: {
type: Number,
min: [0, "Idade não pode ser negativa."],
max: [150, "Idade inválida."],
},
papel: {
type: String,
enum: {
values: ["usuario", "admin", "moderador"],
message: "Papel inválido.",
},
default: "usuario",
},
ativo: {
type: Boolean,
default: true,
},
avatar: {
type: String,
default: null,
},
// Array de strings
habilidades: [String],
// Objeto embutido (embedded)
endereco: {
rua: String,
cidade: String,
estado: { type: String, maxlength: 2 },
cep: String,
},
// Referência a outro documento (relação)
departamento: {
type: Schema.Types.ObjectId,
ref: "Departamento", // nome do Model referenciado
default: null,
},
ultimoAcesso: {
type: Date,
default: null,
},
},
{
// Adiciona createdAt e updatedAt automaticamente
timestamps: true,
// Remove __v do resultado por padrão
versionKey: false,
// Transforma o documento ao converter para JSON
toJSON: {
transform(doc, ret) {
ret.id = ret._id;
delete ret._id;
delete ret.senha; // nunca expõe senha
return ret;
},
},
}
);
// ── Índices ────────────────────────────────────────
usuarioSchema.index({ nome: "text" }); // busca de texto
usuarioSchema.index({ createdAt: -1 }); // ordenação por data
// ── Métodos de instância ───────────────────────────
usuarioSchema.methods.verificarSenha = async function(senhaDigitada) {
const bcrypt = require("bcrypt");
return bcrypt.compare(senhaDigitada, this.senha);
};
usuarioSchema.methods.toPublico = function() {
return {
id: this._id,
nome: this.nome,
email: this.email,
papel: this.papel,
createdAt: this.createdAt,
};
};
// ── Métodos estáticos ──────────────────────────────
usuarioSchema.statics.buscarAtivos = function() {
return this.find({ ativo: true });
};
usuarioSchema.statics.buscarPorEmail = function(email) {
return this.findOne({ email: email.toLowerCase() }).select("+senha");
};
// ── Virtuals — campos calculados ───────────────────
usuarioSchema.virtual("nomeAbreviado").get(function() {
const partes = this.nome.split(" ");
if (partes.length < 2) return this.nome;
return `${partes[0]} ${partes[partes.length - 1][0]}.`;
});
// ── Hooks (middleware do Mongoose) ─────────────────
// Executa ANTES de salvar — ideal para hash de senha
usuarioSchema.pre("save", async function(next) {
// Só faz hash se a senha foi modificada
if (!this.isModified("senha")) return next();
const bcrypt = require("bcrypt");
this.senha = await bcrypt.hash(this.senha, 12);
next();
});
// Executa ANTES de qualquer find — filtro automático
usuarioSchema.pre(/^find/, function(next) {
// Exclui usuários inativos de todas as buscas por padrão
// this.find({ ativo: { $ne: false } });
next();
});
const Usuario = mongoose.model("Usuario", usuarioSchema);
module.exports = Usuario;
CRUD completo com Mongoose
// src/services/usuarioService.js
const Usuario = require("../models/Usuario");
// ── CREATE ──────────────────────────────────────────
async function criar(dados) {
const usuario = new Usuario(dados);
return await usuario.save();
// ou: return await Usuario.create(dados);
}
// Criar múltiplos de uma vez
async function criarVarios(lista) {
return await Usuario.insertMany(lista, { ordered: false });
// ordered: false → continua mesmo se algum falhar
}
// ── READ ────────────────────────────────────────────
// Buscar todos
async function listar(filtros = {}, opcoes = {}) {
const {
pagina = 1,
por_pagina = 10,
ordenar = "-createdAt", // - = decrescente
campos = null, // projeção de campos
} = opcoes;
const query = Usuario.find(filtros);
if (campos) query.select(campos);
query.sort(ordenar);
query.skip((pagina - 1) * por_pagina).limit(Number(por_pagina));
const [dados, total] = await Promise.all([
query.exec(),
Usuario.countDocuments(filtros),
]);
return {
dados,
paginacao: {
total,
pagina: Number(pagina),
por_pagina: Number(por_pagina),
total_paginas: Math.ceil(total / por_pagina),
},
};
}
// Buscar por ID
async function buscarPorId(id) {
const usuario = await Usuario.findById(id);
if (!usuario) throw new Error(`Usuário ${id} não encontrado.`);
return usuario;
}
// Buscar com filtros
async function buscarUm(filtros) {
return await Usuario.findOne(filtros);
}
// Buscar com populate (relações)
async function buscarComDepartamento(id) {
return await Usuario
.findById(id)
.populate("departamento", "nome descricao"); // apenas nome e descricao
}
// Busca textual
async function buscarPorTexto(termo) {
return await Usuario.find(
{ $text: { $search: termo } },
{ score: { $meta: "textScore" } }
).sort({ score: { $meta: "textScore" } });
}
// ── UPDATE ──────────────────────────────────────────
// Atualizar por ID (retorna o documento ANTES da atualização)
async function atualizar(id, dados) {
const usuario = await Usuario.findByIdAndUpdate(
id,
{ $set: dados },
{
new: true, // retorna documento APÓS a atualização
runValidators: true, // executa as validações do schema
}
);
if (!usuario) throw new Error(`Usuário ${id} não encontrado.`);
return usuario;
}
// Atualizar instância (mais controle)
async function atualizarComHooks(id, dados) {
const usuario = await Usuario.findById(id);
if (!usuario) throw new Error(`Usuário ${id} não encontrado.`);
Object.assign(usuario, dados);
return await usuario.save(); // dispara hooks pre/post save
}
// Atualizar muitos
async function ativarTodos() {
const resultado = await Usuario.updateMany(
{ ativo: false },
{ $set: { ativo: true } }
);
return resultado.modifiedCount; // quantos foram atualizados
}
// ── DELETE ──────────────────────────────────────────
// Soft delete (recomendado) — marca como inativo
async function desativar(id) {
return await atualizar(id, { ativo: false });
}
// Hard delete — remove permanentemente
async function remover(id) {
const usuario = await Usuario.findByIdAndDelete(id);
if (!usuario) throw new Error(`Usuário ${id} não encontrado.`);
return usuario;
}
// Remover muitos
async function removerInativos() {
const resultado = await Usuario.deleteMany({ ativo: false });
return resultado.deletedCount;
}
module.exports = {
criar, criarVarios, listar, buscarPorId, buscarUm,
buscarComDepartamento, buscarPorTexto,
atualizar, atualizarComHooks, ativarTodos,
desativar, remover, removerInativos,
};
Operadores de consulta do MongoDB
// Operadores de comparação
await Usuario.find({ idade: { $gte: 18, $lte: 65 } }); // entre 18 e 65
await Usuario.find({ papel: { $in: ["admin", "moderador"] } }); // um dos valores
await Usuario.find({ papel: { $nin: ["usuario"] } }); // não é usuario
await Usuario.find({ avatar: { $ne: null } }); // não é nulo
await Usuario.find({ avatar: { $exists: true } }); // campo existe
// Operadores lógicos
await Usuario.find({
$and: [{ ativo: true }, { idade: { $gte: 18 } }]
});
await Usuario.find({
$or: [{ papel: "admin" }, { idade: { $gte: 60 } }]
});
await Usuario.find({
$nor: [{ ativo: false }, { papel: "usuario" }]
});
// Operadores de array
await Usuario.find({ habilidades: "JavaScript" }); // contém
await Usuario.find({ habilidades: { $in: ["JS", "Python"] } }); // qualquer um
await Usuario.find({ habilidades: { $all: ["JS", "Node"] } }); // todos
await Usuario.find({ habilidades: { $size: 3 } }); // exatamente 3 itens
// Regex
await Usuario.find({ nome: /^ana/i }); // começa com "ana" (case-insensitive)
await Usuario.find({ email: { $regex: "gmail\.com$" } });
// Projeção — escolher campos retornados
await Usuario.find({}).select("nome email -_id"); // inclui nome/email, exclui _id
await Usuario.find({}).select({ nome: 1, email: 1, senha: 0 });
Aggregation Pipeline — consultas avançadas
O Aggregation Pipeline é o recurso mais poderoso do MongoDB — permite transformar e analisar dados em etapas:
// Relatório por departamento
async function relatorioporDepartamento() {
return await Usuario.aggregate([
// Estágio 1: filtrar apenas ativos
{ $match: { ativo: true } },
// Estágio 2: agrupar por departamento
{
$group: {
_id: "$papel",
total: { $sum: 1 },
idadeMedia: { $avg: "$idade" },
idadeMaxima: { $max: "$idade" },
idadeMinima: { $min: "$idade" },
usuarios: { $push: "$nome" },
},
},
// Estágio 3: renomear _id
{
$project: {
papel: "$_id",
total: 1,
idadeMedia: { $round: ["$idadeMedia", 1] },
idadeMaxima: 1,
idadeMinima: 1,
_id: 0,
},
},
// Estágio 4: ordenar por total decrescente
{ $sort: { total: -1 } },
]);
}
// Busca com lookup (similar a JOIN)
async function usuariosComDepartamento() {
return await Usuario.aggregate([
{ $match: { ativo: true } },
{
$lookup: {
from: "departamentos", // nome da coleção (não do Model)
localField: "departamento",
foreignField: "_id",
as: "departamentoInfo",
},
},
{ $unwind: { path: "$departamentoInfo", preserveNullAndEmptyArrays: true } },
{
$project: {
nome: 1,
email: 1,
"departamentoInfo.nome": 1,
},
},
]);
}
Conectando ao Router do Express
// src/routes/usuarios.js
const express = require("express");
const router = express.Router();
const usuarioService = require("../services/usuarioService");
// Wrapper para async/await com tratamento de erros
const asyncHandler = fn => (req, res, next) =>
Promise.resolve(fn(req, res, next)).catch(next);
// GET /usuarios
router.get("/", asyncHandler(async (req, res) => {
const { busca, pagina, por_pagina, papel } = req.query;
const filtros = {};
if (papel) filtros.papel = papel;
if (busca) filtros.$text = { $search: busca };
const resultado = await usuarioService.listar(filtros, { pagina, por_pagina });
res.json(resultado);
}));
// GET /usuarios/:id
router.get("/:id", asyncHandler(async (req, res) => {
const usuario = await usuarioService.buscarPorId(req.params.id);
res.json(usuario);
}));
// POST /usuarios
router.post("/", asyncHandler(async (req, res) => {
const usuario = await usuarioService.criar(req.body);
res.status(201).json(usuario);
}));
// PUT /usuarios/:id
router.put("/:id", asyncHandler(async (req, res) => {
const usuario = await usuarioService.atualizar(req.params.id, req.body);
res.json(usuario);
}));
// DELETE /usuarios/:id
router.delete("/:id", asyncHandler(async (req, res) => {
await usuarioService.remover(req.params.id);
res.json({ mensagem: "Usuário removido com sucesso." });
}));
// Tratamento de erros do Mongoose no error handler global
module.exports = router;
// src/middlewares/erros.js — com tratamento de erros do Mongoose
function tratadorDeErros(erro, req, res, next) {
console.error("[Erro]", erro.message);
// ID inválido (ObjectId malformado)
if (erro.name === "CastError") {
return res.status(400).json({ erro: `ID inválido: ${erro.value}` });
}
// Erro de validação do Mongoose
if (erro.name === "ValidationError") {
const erros = Object.values(erro.errors).map(e => e.message);
return res.status(422).json({ erro: "Dados inválidos.", detalhes: erros });
}
// Duplicidade (chave única violada)
if (erro.code === 11000) {
const campo = Object.keys(erro.keyValue)[0];
return res.status(409).json({
erro: `${campo} já está em uso.`,
});
}
// Erro não encontrado (lançado pelo service)
if (erro.message?.includes("não encontrado")) {
return res.status(404).json({ erro: erro.message });
}
// Erro genérico
res.status(500).json({ erro: "Erro interno do servidor." });
}
module.exports = { tratadorDeErros };
Boas práticas com MongoDB e Mongoose
// ✅ 1. Sempre defina índices para campos consultados frequentemente
usuarioSchema.index({ email: 1 }); // consultas por email
usuarioSchema.index({ createdAt: -1 }); // ordenação por data
usuarioSchema.index({ nome: "text" }); // busca textual
// ✅ 2. Use select("+senha") apenas quando necessário
// select: false no schema protege o campo por padrão
// ✅ 3. Use lean() para consultas somente leitura — muito mais rápido
const usuarios = await Usuario.find({}).lean();
// Retorna objetos JS simples, não documentos Mongoose
// ✅ 4. Prefira soft delete a hard delete
usuarioSchema.add({ deletadoEm: { type: Date, default: null } });
// ✅ 5. Use transações para operações relacionadas
const session = await mongoose.startSession();
session.startTransaction();
try {
await Usuario.create([dados], { session });
await Pedido.create([pedido], { session });
await session.commitTransaction();
} catch (erro) {
await session.abortTransaction();
throw erro;
} finally {
session.endSession();
}
// ✅ 6. Valide ObjectIds antes de consultar
const { isValidObjectId } = mongoose;
if (!isValidObjectId(id)) {
return res.status(400).json({ erro: "ID inválido." });
}
// ✅ 7. Nunca exponha __v e _id diretamente — use toJSON transform
Tarefa para você
Construa um sistema de blog com as seguintes coleções e relacionamentos:
// Coleção: Autor
// { nome, email, bio, avatar, createdAt }
// Coleção: Post
// {
// titulo, conteudo, slug (único, gerado do título),
// autor (ref → Autor), tags: [String],
// publicado: Boolean, visualizacoes: Number,
// createdAt, updatedAt
// }
// Coleção: Comentario
// { post (ref → Post), autor: String, conteudo, aprovado, createdAt }
// Implemente:
// 1. CRUD completo para Autores e Posts
// 2. GET /posts/:slug — busca por slug em vez de ID
// 3. GET /posts com filtro por tag e paginação
// 4. POST /posts/:id/comentarios — adiciona comentário
// 5. Aggregation: relatório com posts mais visualizados por autor
// 6. Hook pre-save para gerar o slug automaticamente do título
// "Meu Post Incrível" → "meu-post-incrivel"
// Use: slug = titulo.toLowerCase().replace(/s+/g, "-").replace(/[^w-]/g, "")
// 7. Virtual: tempoLeitura estimado (palavras / 200 por minuto)
Conclusão
Neste artigo você aprendeu:
- A diferença entre bancos SQL e NoSQL e quando usar cada um
- Como conectar o Node.js ao MongoDB com Mongoose
- Como definir Schemas com tipos, validações e opções avançadas
- Métodos de instância, estáticos, virtuals e hooks
- O CRUD completo — find, create, update, delete e variações
- Todos os operadores de consulta do MongoDB
- O Aggregation Pipeline para análises avançadas
- Como integrar o Mongoose com o Express e tratar seus erros
- Boas práticas — índices, lean, soft delete, transações
No próximo artigo encerramos o Módulo 4 com uma revisão completa e o projeto final do módulo — uma API REST completa com autenticação JWT.
📌 Próximo artigo: Aula 32 — Revisão do Módulo 4 + Projeto: API REST com autenticação
📚 Fontes e Referências
- MongoDB Docs: https://www.mongodb.com/docs
- Mongoose Docs: https://mongoosejs.com/docs
- MongoDB Atlas (gratuito): https://www.mongodb.com/atlas
- MongoDB University (cursos gratuitos): https://learn.mongodb.com
- Mongoose — Queries: https://mongoosejs.com/docs/queries.html
- Mongoose — Middleware: https://mongoosejs.com/docs/middleware.html
- Mongoose — Aggregation: https://mongoosejs.com/docs/api/aggregate.html
- Node.js Design Patterns — Mario Casciaro (Packt Publishing)
- MongoDB: The Definitive Guide — Shannon Bradshaw (O'Reilly)