Node.js: Mongoose ODM

Última atualização: 2026-08-26

O código de validação de dados do Bob, escrito usando o driver nativo do MongoDB, estava espalhado por várias rotas — o endpoint de registro verificava o formato do e-mail, o endpoint de publicação de artigo validava o comprimento do タイトル e o endpoint de alteração de senha verificava se a nova senha era diferente da antiga. Sempre que um novo campo era adicionado, ele precisava incluir um trecho de lógica if (!フィールド) no controlador correspondente, o que fazia com que a mesma expressão regular de validação de e-mail fosse repetida em três rotas. Depois de migrar para o Mongoose, Bob centralizou todas as regras de validação na definição do esquema — definindo-as uma única vez e aplicando-as em todos os lugares —, o que reduziu o código do controlador em 60%.

Você aprenderá:


1. Arquitetura central do Mongoose

O Mongoose é uma biblioteca de mapeamento objeto-documento (ODM) para o MongoDB que oferece uma camada de modelagem de dados orientada por esquema sobre o driver nativo. O conceito central está organizado em três camadas: o esquema define a estrutura → o modelo é compilado em um construtor → um documento é uma instância do modelo.

▶ Exemplo:(1) Relação entre esquema, modelo e documento

100%
graph LR
    A["Schema<br/>Defining Structures and Verification"] -->|mongoose.model() Compilation| B["Model<br/>Constructor + Query Interface"]
    B -->|new Model() Instantiation| C["Document<br/>Examples of Verified Documents"]
    C -->|.save() Persistence| D[("MongoDB<br/>Gathering")]
    B -->|Model.find() etc.| D
    D -->|Back| C

(2) Instalação e conexão

▶ Exemplo: Instalando o Mongoose e conectando-se ao MongoDB

BASH
npm install mongoose
JAVASCRIPT
const mongoose = require('mongoose');

mongoose.connect('mongodb://localhost:27017/myapp')
  .then(() => console.log('MongoDB connected'))
  .catch(err => console.error('Connection error:', err));

(3) Esquema e modelo mínimos

▶ Exemplo: Definindo um esquema de usuário e criando um modelo

JAVASCRIPT
const userSchema = new mongoose.Schema({
  name: String,
  email: String,
  age: Number
});

const User = mongoose.model('User', userSchema);
▶ Experimente
Conceito Função Analogia
Esquema Planta / Definição estrutural Desenhos arquitetônicos
Modelo Construtor + Interface de operação do banco de dados Equipe de construção
Documento Exemplos de documentos verificados Casas concluídas


2. Tipos de campos do esquema

O Mongoose oferece mapeamentos de tipos abrangentes para cada campo, superando em muito a falta de restrições de tipo no driver nativo.

(1) Referência rápida aos tipos de campo

Tipo Mongoose Tipo JS correspondente Exemplo Descrição
String String name: String Corte automático (requer configuração)
Number Número age: Number Suporta mínimo/máximo
Boolean Booleano active: Boolean Converte automaticamente para 0/1/"true"
Date Data createdAt: Date Métodos de data integrados
ObjectId ObjectId author: mongoose.Schema.Types.ObjectId Referências a outros documentos
Array Matriz tags: [String] Matriz de documentos filhos ou matriz de tipos
Mixed Objeto meta: mongoose.Schema.Types.Mixed Qualquer tipo, sem validação
Buffer Buffer avatar: Buffer Dados binários
Map Mapa prefs: { type: Map, of: String } Estrutura do Mapa no ES6
Decimal128 Decimal128 price: mongoose.Schema.Types.Decimal128 Decimal de alta precisão

(2) A sintaxe completa para definições de campos

▶ Exemplo: Explicação detalhada das opções de campo

JAVASCRIPT
const productSchema = new mongoose.Schema({
  name: {
    type: String,
    required: [true, 'The product name cannot be left blank.'],
    trim: true,
    minlength: 2,
    maxlength: 100
  },
  price: {
    type: Number,
    required: true,
    min: [0, 'Prices cannot be negative.'],
    default: 0
  },
  category: {
    type: String,
    enum: ['electronics', 'books', 'clothing', 'food'],
    lowercase: true
  },
  tags: [String],
  metadata: {
    type: mongoose.Schema.Types.Mixed,
    default: {}
  }
});
▶ Experimente

3. Validador

A validação é o cerne do Mongoose — ela libera os controladores da tarefa de validar dados e centraliza essa tarefa nas declarações de esquema.

(1) Referência rápida do validador

Validador Tipo aplicável Descrição Exemplo
required Todos Campos obrigatórios required: [true, 'Cannot be empty']
enum String Restrição de valor de enumeração enum: ['A', 'B', 'C']
min Número / Data Valor mínimo min: 0
max Número / Data Valor máximo max: 150
minlength Sequência Comprimento mínimo minlength: 6
maxlength Sequência de caracteres Comprimento máximo maxlength: 200
match String Correspondência de expressão regular match: [/^\S+@\S+\.\S+$/, 'Invalid email format']
validate Todas Funções de validação personalizadas validate: v => v > 0

(2) Validadores personalizados

▶ Exemplo: Validadores personalizados e mensagens de erro

JAVASCRIPT
const userSchema = new mongoose.Schema({
  password: {
    type: String,
    required: true,
    validate: {
      validator: function(v) {
        return /^(?=.*[A-Z])(?=.*\d).{8,}$/.test(v);
      },
      message: props => `${props.value} Password does not meet requirements: at least 8 characters, includes uppercase and digits`
    }
  },
  phone: {
    type: String,
    validate: {
      validator: function(v) {
        return /^1[3-9]\d{9}$/.test(v);
      },
      message: 'The phone number format is incorrect'
    }
  }
});
▶ Experimente

(3) Verifique o tempo de acionamento

A verificação é acionada automaticamente nos seguintes momentos: new Model().save() e Model.create(). Você pode acioná-la manualmente usando o método validate(). updateOne() / updateMany(), etc., não acionam automaticamente a verificação; é necessário configurar a opção runValidators: true.

▶ Exemplo: Ativando a validação para operações de atualização

JAVASCRIPT
User.updateOne(
  { email: 'bob@test.com' },
  { age: -5 },
  { runValidators: true }
);
▶ Experimente

4. Propriedades virtuais

Os atributos virtuais não são armazenados no banco de dados; eles são calculados dinamicamente apenas durante as consultas, o que os torna adequados para campos derivados.

(1) Definição e uso

▶ Exemplo: atributo virtual “Nome completo do usuário”

JAVASCRIPT
const userSchema = new mongoose.Schema({
  firstName: String,
  lastName: String,
  email: String
});

userSchema.virtual('fullName')
  .get(function() {
    return `${this.firstName} ${this.lastName}`;
  })
  .set(function(v) {
    const parts = v.split(' ');
    this.firstName = parts[0];
    this.lastName = parts[1] || '';
  });

const User = mongoose.model('User', userSchema);
const user = new User({ firstName: 'Bob', lastName: 'Smith' });
console.log(user.fullName);
▶ Experimente
TEXT 📖 Somente leitura
Bob Smith

(2) virtual e toJSON

Os atributos virtuais não são incluídos por padrão nas saídas toJSON() e toObject(). Eles devem ser ativados explicitamente nas opções do esquema:

JAVASCRIPT
const userSchema = new mongoose.Schema({
  firstName: String,
  lastName: String
}, {
  toJSON: { virtuals: true },
  toObject: { virtuals: true }
});


5. Hooks (Middleware)

Os hooks (middleware) são executados automaticamente em etapas específicas do ciclo de vida de um documento e são utilizados para pré-processamento de dados, registro em log, operações em cascata e muito mais.

(1) Tipos de hook e condições de acionamento

Tipo de gancho Condições de acionamento Usos comuns
pre('save') Antes de salvar Hash da senha, formatação dos dados, carimbo de data/hora da atualização
post('save') Após salvar Enviar notificações, registrar entradas
pre('remove') Antes da exclusão Exclusão em cascata de documentos associados
post('remove') Após a exclusão Limpar recursos e registros
pre('find') Antes da consulta Critérios de filtragem padrão (por exemplo, exclusão temporária)
post('find') Após a consulta Anonimização de dados
pre('updateOne') Antes da atualização Data e hora da atualização
post('aggregate') Após a agregação Registro

(2) para ganchos

▶ Exemplo: Criar hash das senhas automaticamente antes de salvá-las

JAVASCRIPT
const bcrypt = require('bcrypt');

userSchema.pre('save', async function(next) {
  if (!this.isModified('password')) return next();
  this.password = await bcrypt.hash(this.password, 10);
  next();
});
▶ Experimente

(3) Ganchos de postagem

▶ Exemplo: Enviar um e-mail de boas-vindas após o salvamento

JAVASCRIPT
userSchema.post('save', function(doc, next) {
  console.log(`User ${doc.email} Saved`);
  next();
});
▶ Experimente

(4) Ganchos de consulta

▶ Exemplo: Filtrar automaticamente documentos excluídos durante uma consulta

JAVASCRIPT
userSchema.pre('find', function() {
  this.where({ deletedAt: null });
});
▶ Experimente

6. Construtor de consultas

O construtor de consultas do Mongoose suporta chamadas encadeadas, que são mais intuitivas do que os parâmetros de objeto utilizados pelo driver nativo.

(1) Método de consulta em cadeia

▶ Exemplo: Chamadas encadeadas do Construtor de Consultas

JAVASCRIPT
const users = await User.find()
  .where('age').gte(18).lte(65)
  .where('role').equals('admin')
  .sort({ createdAt: -1 })
  .select('name email age')
  .limit(10)
  .skip(0);

console.log(users);
▶ Experimente

(2) Comparação de métodos comuns de consulta

Implementação de driver nativo Construtor de consultas do Mongoose Descrição
db.users.find({ age: { $gte: 18 } }) User.find().where('age').gte(18) Pesquisa por condição
db.users.find().sort({ name: 1 }) User.find().sort({ name: 1 }) Ordenar
db.users.find().limit(10) User.find().limit(10) Número de inscrições limitado a
db.users.find().skip(20) User.find().skip(20) Pular
db.users.find({}, { name: 1 }) User.find().select('name') Filtro de campo

(3) Encapsulamento de consultas paginadas

▶ Exemplo: Funções auxiliares para consultas paginadas

JAVASCRIPT
async function paginate(Model, filter = {}, page = 1, limit = 10) {
  const skip = (page - 1) * limit;
  const [docs, total] = await Promise.all([
    Model.find(filter).skip(skip).limit(limit).sort({ createdAt: -1 }),
    Model.countDocuments(filter)
  ]);
  return {
    data: docs,
    total,
    page,
    totalPages: Math.ceil(total / limit)
  };
}

const result = await paginate(User, { role: 'user' }, 2, 10);
▶ Experimente

7. Índice

Os índices são fundamentais para o desempenho das consultas em bancos de dados. O Mongoose oferece suporte à definição declarativa de índices no esquema.

(1) Índices de coluna única e índices compostos

▶ Exemplo: Definindo um índice no esquema

JAVASCRIPT
const userSchema = new mongoose.Schema({
  email: {
    type: String,
    required: true,
    unique: true
  },
  username: {
    type: String,
    index: true
  },
  region: String,
  status: String
});

userSchema.index({ region: 1, status: 1 });
▶ Experimente
Tipo de índice Definição Descrição
Índice exclusivo unique: true Os valores dos campos devem ser exclusivos
Índice normal index: true Consulta acelerada
Índice Composto schema.index({ a: 1, b: -1 }) Índice Composto Multidisciplinar
Índice de textos schema.index({ title: 'text' }) Pesquisa de texto completo

▶ Exemplo:(2) Criação automática de índices no ambiente de desenvolvimento

JAVASCRIPT
mongoose.connect(uri, { autoIndex: true });
▶ Experimente

Em um ambiente de produção, recomenda-se desativar autoIndex e criar índices manualmente usando o script de migração para evitar atrasos na inicialização.



8. Métodos de instância e métodos estáticos

O Mongoose permite que você amplie um esquema com métodos personalizados, que se dividem em duas categorias: métodos de instância e métodos estáticos.

(1) Métodos de instância

Os métodos de instância atuam sobre um único documento; use this para acessar o documento atual.

▶ Exemplo: Método de comparação de senhas

JAVASCRIPT
userSchema.methods.comparePassword = function(candidate) {
  return bcrypt.compare(candidate, this.password);
};

const user = await User.findOne({ email: 'bob@test.com' });
const isMatch = await user.comparePassword('mypassword');
▶ Experimente

(2) Métodos estáticos

Os métodos estáticos são definidos no Modelo e não dependem de instâncias de documentos, o que os torna adequados para o suporte a consultas.

▶ Exemplo: Localizando métodos estáticos por função

JAVASCRIPT
userSchema.statics.findByRole = function(role) {
  return this.find({ role }).sort({ createdAt: -1 });
};

const admins = await User.findByRole('admin');
▶ Experimente
Tipo Definição Chamada Esta referência
Métodos de instância schema.methods.xxx = function doc.xxx() Instância de documento
Método estático schema.statics.xxx = function Model.xxx() Modelo


9. Preencher consultas com junções

O populate() do Mongoose implementa a resolução de referências entre documentos do MongoDB, de forma semelhante a um JOIN do SQL.

(1) Definições de referência e junções

▶ Exemplo: Artigos associados a autores

JAVASCRIPT
const postSchema = new mongoose.Schema({
  title: String,
  content: String,
  author: {
    type: mongoose.Schema.Types.ObjectId,
    ref: 'User',
    required: true
  }
});

const Post = mongoose.model('Post', postSchema);

const posts = await Post.find().populate('author', 'firstName lastName email');
▶ Experimente

(2) Preenchimento em vários níveis e filtragem condicional

▶ Exemplo: Junções e filtros em vários níveis

JAVASCRIPT
const commentSchema = new mongoose.Schema({
  content: String,
  author: { type: mongoose.Schema.Types.ObjectId, ref: 'User' },
  post: { type: mongoose.Schema.Types.ObjectId, ref: 'Post' }
});

const Comment = mongoose.model('Comment', commentSchema);

const comments = await Comment.find()
  .populate('author', 'firstName lastName')
  .populate({
    path: 'post',
    select: 'title content',
    match: { status: 'published' }
  });
▶ Experimente

(3) Considerações sobre desempenho para populate

populate() Essencialmente, isso envolve a emissão de consultas adicionais e, em seguida, a fusão dos resultados; não se trata de um JOIN propriamente dito. O problema N+1 ainda persiste — se 100 artigos estiverem associados a 100 autores diferentes, isso acionará 101 consultas. Para cenários com associações frequentes, considere a incorporação de documentos ou $lookup a agregação.



10. Comparação entre o Mongoose e o driver nativo

Dimensão Driver nativo do MongoDB Mongoose ODM
Validação de dados Instruções if/else codificadas manualmente espalhadas pelo controlador Validação declarativa baseada em esquema, gerenciada centralmente
Restrições de tipo Nenhuma; qualquer campo pode armazenar qualquer valor Tipos impostos pelo esquema; conversão automática
Consultas combinadas Agregação manual $lookup populate() Feito em uma linha
Ganchos de ciclo de vida Nenhum Ganchos pré/pós
Atributo virtual Nenhum Campo virtual calculado dinamicamente
Gerenciamento de índices Manual createIndex() Declaração de esquema + Criação automática
API de consulta Parâmetros de objeto find({ age: { $gte: 18 } }) Construtor encadeado + parâmetros de objeto
Curva de aprendizado Baixa; se assemelha bastante à sintaxe nativa do MongoDB Média; requer compreensão de esquema, modelo e documento
Flexibilidade Alta, controle total Média, os campos fora do esquema são ignorados por padrão
Desempenho Ligeiramente melhor, sem camada intermediária Ligeiramente menor, sobrecarga causada pela validação e pelos hooks


11. Exemplo abrangente: modelos de dados de usuários e artigos

Combine esquemas, validação, hooks, atributos virtuais, métodos de instância e consultas de junção para formar um sistema completo de modelo de dados.

▶ Exemplo: models/User.js

JAVASCRIPT 📖 Somente leitura
const mongoose = require('mongoose');
const bcrypt = require('bcrypt');

const userSchema = new mongoose.Schema({
  firstName: {
    type: String,
    required: [true, 'The last name cannot be left blank.'],
    trim: true
  },
  lastName: {
    type: String,
    required: [true, 'The name cannot be empty'],
    trim: true
  },
  email: {
    type: String,
    required: [true, 'The email address cannot be left blank.'],
    unique: true,
    lowercase: true,
    match: [/^\S+@\S+\.\S+$/, 'The email address format is incorrect.']
  },
  password: {
    type: String,
    required: [true, 'The password cannot be empty.'],
    minlength: 8,
    validate: {
      validator: function(v) {
        return /^(?=.*[A-Z])(?=.*\d).{8,}$/.test(v);
      },
      message: 'Password must be at least 8 characters, must include uppercase and digits'
    }
  },
  role: {
    type: String,
    enum: ['user', 'admin'],
    default: 'user'
  },
  age: {
    type: Number,
    min: [0, 'Age cannot be a negative number.'],
    max: [150, 'Age must not exceed150']
  },
  createdAt: {
    type: Date,
    default: Date.now
  }
}, {
  toJSON: { virtuals: true },
  toObject: { virtuals: true }
});

userSchema.virtual('fullName').get(function() {
  return `${this.firstName} ${this.lastName}`;
});

userSchema.pre('save', async function(next) {
  if (!this.isModified('password')) return next();
  this.password = await bcrypt.hash(this.password, 10);
  next();
});

userSchema.methods.comparePassword = function(candidate) {
  return bcrypt.compare(candidate, this.password);
};

userSchema.statics.findByRole = function(role) {
  return this.find({ role }).sort({ createdAt: -1 });
};

module.exports = mongoose.model('User', userSchema);
64 linhas de lógica (limite de 40, somente leitura)

▶ Exemplo: models/Post.js

JAVASCRIPT 📖 Somente leitura
const mongoose = require('mongoose');

const postSchema = new mongoose.Schema({
  title: {
    type: String,
    required: [true, 'The title cannot be left blank.'],
    trim: true,
    minlength: [2, 'Title must be at least2characters'],
    maxlength: [200, 'Most Titles200characters']
  },
  content: {
    type: String,
    required: [true, 'Content cannot be empty']
  },
  author: {
    type: mongoose.Schema.Types.ObjectId,
    ref: 'User',
    required: true
  },
  status: {
    type: String,
    enum: ['draft', 'published', 'archived'],
    default: 'draft'
  },
  tags: [{
    type: String,
    lowercase: true
  }],
  viewCount: {
    type: Number,
    default: 0,
    min: 0
  },
  createdAt: {
    type: Date,
    default: Date.now
  },
  updatedAt: {
    type: Date,
    default: Date.now
  }
});

postSchema.index({ status: 1, createdAt: -1 });
postSchema.index({ tags: 1 });

postSchema.virtual('excerpt').get(function() {
  return this.content.substring(0, 100) + '...';
});

postSchema.pre('save', function(next) {
  if (this.isModified('content')) {
    this.updatedAt = new Date();
  }
  next();
});

postSchema.post('remove', async function(doc) {
  await mongoose.model('Comment').deleteMany({ post: doc._id });
});

postSchema.statics.findPublished = function() {
  return this.find({ status: 'published' })
    .populate('author', 'firstName lastName email')
    .sort({ createdAt: -1 });
};

module.exports = mongoose.model('Post', postSchema);
61 linhas de lógica (limite de 40, somente leitura)

▶ Exemplo: Consultas e junções

JAVASCRIPT
const mongoose = require('mongoose');
const User = require('./models/User');
const Post = require('./models/Post');

async function main() {
  await mongoose.connect('mongodb://localhost:27017/blog');

  const user = await User.create({
    firstName: 'Bob',
    lastName: 'Smith',
    email: 'bob@example.com',
    password: 'Secure123',
    role: 'admin',
    age: 28
  });

  const post = await Post.create({
    title: 'Mongoose Getting Started Guide',
    content: 'Mongoose is MongoDB ODM library, providing schema-driven data modeling...',
    author: user._id,
    status: 'published',
    tags: ['mongodb', 'mongoose', 'nodejs']
  });

  const published = await Post.findPublished();
  console.log(published[0].excerpt);
  console.log(published[0].author.fullName);

  const match = await user.comparePassword('Secure123');
  console.log('Password match:', match);

  await mongoose.connection.close();
}

main();
▶ Experimente
BASH
node app.js
TEXT 📖 Somente leitura
Mongoose is MongoDB ODM library, providing schema-driven data modeling......
Bob Smith
Password match: true

❓ Perguntas Frequentes

P: Como faço para escolher entre o Mongoose e o driver nativo? R: Escolha o driver nativo para projetos pequenos ou quando precisar de flexibilidade máxima; escolha o Mongoose para projetos de médio a grande porte, colaboração em equipe ou quando precisar de validação e hooks. Você também pode usar os dois juntos — o Mongoose oferece suporte ao acesso a coleções nativas por meio do Model.collection.

P: Qual é a diferença entre um Esquema e um Modelo? R: Um Esquema é um modelo que define tipos de campos, regras de validação e hooks; um Modelo é um construtor gerado a partir de um Esquema que fornece uma interface para métodos CRUD. Um único Esquema pode gerar apenas um Modelo.

P: Os campos virtuais não são armazenados no banco de dados? R: Sim, os campos virtuais são calculados apenas na memória e não são gravados no MongoDB. Se você precisar mantê-los, use um campo comum e o hook pre-save para calcular e atribuir o valor. Por padrão, os campos virtuais não aparecem na saída JSON; é necessário definir toJSON: { virtuals: true }.

P: Qual é o desempenho do populate? R: O populate não é um JOIN de SQL; ele basicamente envolve a execução de consultas adicionais e, em seguida, a fusão dos resultados. O desempenho é bom quando há poucos documentos relacionados; no entanto, um grande número de documentos relacionados diferentes pode causar um problema de consulta N+1. Para cenários de alta frequência, considere a incorporação de documentos, o uso manual do $lookup ou o armazenamento em cache.

P: Como faço para lidar com erros de validação? R: O Mongoose lança ValidationError quando a validação falha, e o objeto errors contém os detalhes do erro para cada campo. Você pode usar Object.values(err.errors).map(e => e.message) para extrair todas as mensagens e, em conjunto com o middleware de tratamento de erros do Express, retornar um código de status 400 uniforme.

P: A que se refere this no hook pre-save? R: this refere-se à instância Document que está prestes a ser salva. Observação: o uso de funções-seta fará com que a ligação this seja perdida; a função do gancho deve ser uma função comum. this.isModified('field') pode ser usado para determinar se um campo foi modificado.

P: Posso adicionar campos depois que o esquema já tiver sido definido? R: Sim, você pode usar schema.add({ newField: String }) para adicioná-los dinamicamente. No entanto, os modelos compilados não serão atualizados automaticamente; você precisará recompilá-los ou usar a extensão schema.plugin(). Recomendamos planejar cuidadosamente a estrutura dos campos no início do projeto.


📖 Resumo


📝 Exercícios

  1. Conclua todos os exemplos de código desta lição e certifique-se de que cada um deles seja executado corretamente.
  2. Modifique o exemplo completo e adicione suas próprias extensões
  3. Analise a documentação oficial, identifique 1 ou 2 APIs que não foram abordadas nesta aula e escreva um código de teste para elas.
  4. Reflexão: Como você aplicaria o que aprendeu nesta aula a um projeto do mundo real?
  5. Tente combinar o que você aprendeu nesta aula com o conteúdo das aulas anteriores para criar um pequeno projeto.
Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%