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 mongooseO 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.




