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

ESLint e Prettier: código limpo e padronizado
ESLint e Prettier: código limpo e padronizado

Imagine entrar em um projeto onde cada arquivo tem um estilo diferente — aspa...

Estado global com Zustand e React Query
Estado global com Zustand e React Query

Nos artigos anteriores você aprendeu a gerenciar estado local com useState e...

Criando e Removendo Elementos Dinamicamente
Criando e Removendo Elementos Dinamicamente

Nos artigos anteriores aprendemos a selecionar elementos existentes no HTML e...