Home > Blog > Desenvolvimento Web
Desenvolvimento Web
JavaScript
Programação

Mongoose no Node.js: Guia Prático

Atualizado em: 4 de setembro de 2026

Estação de trabalho usada no desenvolvimento de aplicações Node.js

Usar Mongoose no Node.js adiciona schemas, validação, middleware, models e recursos de modelagem sobre o MongoDB. A biblioteca funciona como um ODM: documentos do banco são representados por objetos JavaScript com regras, métodos e transformações definidas pela aplicação.

Mongoose é útil quando a equipe quer uma camada consistente para validar documentos, aplicar defaults, criar índices, organizar relações e manter hooks de persistência. Porém, schemas e middleware não substituem uma boa modelagem do MongoDB. Plugins excessivos, populate indiscriminado e documentos gigantes ainda podem comprometer desempenho.

Neste guia, você aprenderá conexão, schemas, models, validação, middleware, queries, populate, lean, índices, transações, TypeScript, repositories, segurança, testes e shutdown.

O que é Mongoose?

Mongoose é uma biblioteca de modelagem de objetos para MongoDB em Node.js. A documentação oficial do Mongoose apresenta schemas, models, documents, queries, middleware, populate, transactions e suporte a TypeScript. Para entender a API nativa do banco, consulte também a documentação do driver oficial do MongoDB.

Este artigo complementa MongoDB no Node.js com o driver oficial. Para separar persistência do domínio, veja Repository Pattern no Node.js.

Instalação

npm install mongoose

O pacote já inclui definições TypeScript.

Conectando ao MongoDB

import mongoose from 'mongoose';

await mongoose.connect(process.env.MONGODB_URI!, {
  dbName: 'orders',
  appName: 'orders-api',
  serverSelectionTimeoutMS: 5000,
  maxPoolSize: 50
});

Abra a conexão uma vez durante o bootstrap. Não chame connect() em cada requisição.

Eventos de conexão

mongoose.connection.on('connected', () => {
  logger.info('MongoDB conectado');
});

mongoose.connection.on('error', error => {
  logger.error({ err: error }, 'Erro no MongoDB');
});

mongoose.connection.on('disconnected', () => {
  logger.warn('MongoDB desconectado');
});

Evite registrar a URI, pois ela pode conter credenciais.

Schema básico

import { Schema, model, Types } from 'mongoose';

type OrderStatus = 'pending' | 'approved' | 'cancelled';

interface OrderDocument {
  customerId: Types.ObjectId;
  status: OrderStatus;
  totalCents: number;
  version: number;
  createdAt: Date;
  updatedAt: Date;
}

const orderSchema = new Schema<OrderDocument>({
  customerId: {
    type: Schema.Types.ObjectId,
    required: true,
    index: true
  },
  status: {
    type: String,
    enum: ['pending', 'approved', 'cancelled'],
    required: true,
    default: 'pending'
  },
  totalCents: {
    type: Number,
    required: true,
    min: 0
  },
  version: {
    type: Number,
    required: true,
    default: 1
  }
}, {
  timestamps: true,
  strict: 'throw'
});

timestamps adiciona createdAt e updatedAt. strict: 'throw' rejeita campos desconhecidos em vez de descartá-los silenciosamente.

Compilando o model

export const OrderModel = model<OrderDocument>(
  'Order',
  orderSchema
);

O nome do model é singular; Mongoose normalmente resolve o nome da collection.

Cuidado com hot reload

Ambientes serverless ou desenvolvimento podem recompilar o model:

export const OrderModel =
  mongoose.models.Order ||
  mongoose.model('Order', orderSchema);

Use esse padrão apenas quando o runtime realmente recarrega módulos.

Criando um documento

const order = await OrderModel.create({
  customerId,
  status: 'pending',
  totalCents: 15990
});

create() aplica casting, defaults, validação e middleware de save.

Validação

const orderSchema = new Schema({
  externalId: {
    type: String,
    required: true,
    trim: true,
    minlength: 8,
    maxlength: 100
  },
  totalCents: {
    type: Number,
    required: true,
    validate: {
      validator(value: number) {
        return Number.isSafeInteger(value) && value >= 0;
      },
      message: 'totalCents deve ser inteiro não negativo'
    }
  }
});

A validação de Mongoose protege a aplicação, mas regras críticas também podem existir no banco e no domínio.

Validação de update

Queries de update não executam todas as validações automaticamente em todas as situações. Use:

await OrderModel.updateOne(
  { _id: orderId },
  { $set: { status: input.status } },
  { runValidators: true }
);

Defina uma convenção para não esquecer essa opção.

Validando entrada externa

Não passe req.body diretamente ao model. Valide e construa um objeto permitido:

const input = createOrderSchema.parse(req.body);

await OrderModel.create({
  customerId: req.user.id,
  totalCents: input.totalCents
});

Para JSON Schema, consulte Ajv no Node.js.

Queries

const order = await OrderModel.findById(orderId);

const pending = await OrderModel.find({
  customerId,
  status: 'pending'
})
  .sort({ createdAt: -1, _id: -1 })
  .limit(20);

Queries de Mongoose são entãoáveis, mas execute com await ou exec() uma única vez.

exec()

const order = await OrderModel
  .findById(orderId)
  .exec();

exec() deixa explícito que a query será executada.

lean()

const orders = await OrderModel.find({ customerId })
  .select('status totalCents createdAt')
  .lean()
  .exec();

lean() retorna objetos simples, sem hidratação, métodos, virtuals ou change tracking. Use em consultas somente leitura para reduzir CPU e memória.

Quando não usar lean

Não use quando precisa de methods, getters, virtuals ou save(). O retorno não é um document Mongoose.

Projection

const user = await UserModel.findById(userId)
  .select('name email plan')
  .lean();

Selecione apenas campos necessários. Marque segredos como select: false no schema.

Campos sensíveis

passwordHash: {
  type: String,
  required: true,
  select: false
}

Para buscar explicitamente:

const user = await UserModel
  .findOne({ email })
  .select('+passwordHash');

Não serialize o document inteiro em logs.

Métodos de documento

orderSchema.methods.approve = function approve() {
  if (this.status !== 'pending') {
    throw new InvalidOrderStatusError();
  }

  this.status = 'approved';
};

Adicione methods antes de compilar o model.

Statics

orderSchema.static(
  'findPendingByCustomer',
  function (customerId: Types.ObjectId) {
    return this.find({
      customerId,
      status: 'pending'
    });
  }
);

Em projetos grandes, repositories costumam fornecer uma fronteira mais clara que muitos statics.

Virtuals

orderSchema.virtual('total').get(function () {
  return this.totalCents / 100;
});

Virtual não é persistido. Para incluir em JSON:

new Schema(definition, {
  toJSON: { virtuals: true }
});

Evite transformar valores monetários de maneira ambígua; DTOs explícitos podem ser melhores.

Middleware pre e post

orderSchema.pre('save', function () {
  if (this.isModified('status')) {
    this.updatedAt = new Date();
  }
});

Middleware é útil, mas pode esconder efeitos. Não envie e-mail ou publique fila dentro de um hook de save.

Hooks e transações

Um hook que grava em outra collection precisa receber a mesma session. Caso contrário, fica fora da transação.

Middleware de query

orderSchema.pre('find', function () {
  this.where({ deletedAt: null });
});

Soft delete global pode ser conveniente, mas torna consultas administrativas e índices mais difíceis. Documente escape hatches.

Populate

const order = await OrderModel.findById(orderId)
  .populate({
    path: 'customerId',
    select: 'name email'
  });

populate executa consultas adicionais ou estratégias específicas. Ele não é uma join gratuita.

Evite populate em cascata

Populate profundo pode criar muitas queries e payloads. Prefira:

  • denormalização controlada;
  • aggregation com $lookup;
  • read models;
  • batching;
  • queries específicas.

Subdocuments

const itemSchema = new Schema({
  productId: Schema.Types.ObjectId,
  nameSnapshot: String,
  quantity: Number,
  priceCents: Number
}, { _id: false });

orderSchema.add({
  items: {
    type: [itemSchema],
    validate: value => value.length > 0
  }
});

Subdocuments funcionam bem quando têm o mesmo ciclo de vida do agregado e tamanho limitado.

Discriminators

Discriminators compartilham collection e schema base:

const EventModel = model('Event', eventSchema);

const PaymentEvent = EventModel.discriminator(
  'payment',
  paymentEventSchema
);

Use quando tipos compartilham estrutura e estratégia de consulta. Não crie uma hierarchy difícil de migrar sem necessidade.

Índices

orderSchema.index({
  customerId: 1,
  createdAt: -1
});

orderSchema.index(
  { externalId: 1 },
  { unique: true }
);

unique não é validator; ele cria índice unique no MongoDB. Trate duplicate key.

autoIndex

Criar índices automaticamente em produção pode causar impacto. Uma estratégia comum é desabilitar e gerenciar índices em deploy:

mongoose.set('autoIndex', false);

Compare schemas e índices reais antes de alterar.

Paginação

const filter = cursor
  ? { _id: { $lt: new Types.ObjectId(cursor) } }
  : {};

const items = await OrderModel.find(filter)
  .sort({ _id: -1 })
  .limit(limit + 1)
  .lean();

Consulte Paginação em APIs Node.js.

Transações

const session = await mongoose.startSession();

try {
  await session.withTransaction(async () => {
    await OrderModel.create([order], { session });
    await OutboxModel.create([event], { session });
  });
} finally {
  await session.endSession();
}

Passe session em todas as operações envolvidas.

Unit of Work

Uma Unit of Work pode encapsular a session e repositories Mongoose. Consulte Unit of Work no Node.js.

Não faça Promise.all em transação sem entender

Operações concorrentes na mesma session podem ter limitações. Prefira sequência clara dentro do callback transacional.

Lock otimista

Mongoose possui version key, normalmente __v, e opções de optimistic concurrency:

const schema = new Schema(definition, {
  optimisticConcurrency: true
});

Confirme o comportamento da versão usada e trate VersionError. Consulte Lock Otimista no Node.js.

Repository com Mongoose

export class MongooseOrderRepository
  implements OrderRepository {

  async findById(id: OrderId) {
    const doc = await OrderModel.findById(id.value)
      .lean()
      .exec();

    return doc
      ? OrderMapper.toDomain(doc)
      : null;
  }

  async save(order: Order) {
    const data = OrderMapper.toPersistence(order);

    await OrderModel.updateOne(
      {
        _id: data.id,
        version: data.version
      },
      {
        $set: data.fields,
        $inc: { version: 1 }
      },
      { runValidators: true }
    );
  }
}

O domínio não precisa importar Document ou Model.

TypeScript

Evite interfaces divergentes do schema. Teste tipos, use inferência quando adequada e considere tipos separados para documento, entrada e domínio.

Schema não é domínio

Um schema Mongoose representa persistência. Entidades e Value Objects podem ter regras independentes. Consulte Value Objects no Node.js.

Segurança de queries

Não passe filtros vindos do cliente:

// inseguro
OrderModel.find(req.body.filter);

Construa allowlists e valide tipos para evitar NoSQL injection.

sanitizeFilter

Mongoose oferece opções de sanitização em determinados cenários, mas elas não substituem schemas de entrada e filtros explícitos.

Logs e debug

mongoose.set('debug', false);

Debug pode incluir filtros e dados. Não habilite indiscriminadamente em produção.

Observabilidade

Meça:

  • tempo de query;
  • pool;
  • timeouts;
  • erros de validação;
  • duplicate key;
  • VersionError;
  • queries sem índice;
  • documentos retornados.

Shutdown

await mongoose.disconnect();

Feche após parar novas requisições e aguardar operações. Consulte Graceful Shutdown no Node.js.

Testes

Use MongoDB real em container para testar:

  • validação;
  • índices unique;
  • middleware;
  • populate;
  • transactions;
  • mapeamento;
  • optimistic concurrency;
  • queries lean.

Mocks de model

Mocks de chains Mongoose são frágeis. Teste casos de uso com repository fake e o adapter Mongoose com integração.

Erros comuns

  • Model por request: ocorre recompilação e vazamento.
  • req.body direto: campos e operadores indesejados entram.
  • Populate excessivo: desempenho cai.
  • Sem lean em leitura: CPU e memória aumentam.
  • Middleware com efeitos externos: dual write aparece.
  • unique tratado como validação: race condition permanece.
  • Session esquecida: operação sai da transação.
  • Schema como domínio: infraestrutura se espalha.

Boas práticas

  • Conecte uma vez.
  • Defina schemas estritos.
  • Valide updates.
  • Use projection e lean.
  • Gerencie índices.
  • Controle populate.
  • Passe session explicitamente.
  • Use repositories quando necessário.
  • Proteja filtros.
  • Teste com MongoDB real.

Conclusão

Usar Mongoose no Node.js adiciona schemas, validação, middleware e models sobre o MongoDB. A biblioteca melhora consistência quando usada com queries específicas, índices e regras claras.

O maior cuidado é não transformar hooks e populate em comportamento invisível. Com lean, repositories, transações explícitas, filtros seguros e testes de integração, Mongoose oferece produtividade sem esconder completamente o banco.

10 melhores cursos de programação em 2026

Descubra os melhores cursos de programação. Aprenda a escolher o curso ideal para iniciar ou avançar na carreira de desenvolvedor

POSTS RELACIONADOS

Ver todos

Seta para a direita