Javascript

MongoDB e Mongoose: banco de dados com Node Já leu

13 min de leitura

MongoDB e Mongoose: banco de dados com Node
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

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)
Comentários

Mais em Javascript

React Router: navegação em SPAs
React Router: navegação em SPAs

Até agora construímos componentes React que vivem em uma única página. Mas ap...

Projeto: API REST com autenticação JWT
Projeto: API REST com autenticação JWT

Chegamos ao fim do Módulo 4. Em seis artigos você saiu do zero no Node.js até...

Trabalhando com JSON
Trabalhando com JSON

JSON está em absolutamente tudo. É o formato de dados mais usado na web — em...