Usar MongoDB no Node.js com o driver oficial oferece acesso direto a collections, documentos, índices, aggregation pipelines, transações e change streams. A aplicação trabalha com a API nativa do banco sem uma camada ODM obrigatória.
O driver é adequado quando a equipe quer controle explícito sobre queries e modelos, precisa usar recursos recentes do MongoDB ou prefere evitar abstrações adicionais. Em troca, validação, mapeamento e organização da persistência ficam sob responsabilidade da aplicação.
Neste guia, você aprenderá conexão, pooling, CRUD, ObjectId, índices, paginação, agregações, transações, sessões, segurança, TypeScript, testes e shutdown com o driver oficial.
Driver oficial do MongoDB
A documentação atual do MongoDB Node.js Driver cobre Atlas, Enterprise e Community. No momento da redação, a documentação marca a linha 7.x como atual.
Este artigo complementa o conteúdo introdutório O que é MongoDB, focando implementação em Node.js.
Instalação
npm install mongodbEm TypeScript, os tipos já acompanham o pacote.
Conexão
import { MongoClient } from 'mongodb';
const client = new MongoClient(
process.env.MONGODB_URI,
{
appName: 'orders-api'
}
);
await client.connect();
const db = client.db('orders');Crie um cliente por processo, não um por requisição.
Connection pool
MongoClient gerencia pool internamente. Opções relevantes incluem:
const client = new MongoClient(uri, {
maxPoolSize: 50,
minPoolSize: 5,
maxIdleTimeMS: 30000,
waitQueueTimeoutMS: 2000,
serverSelectionTimeoutMS: 5000
});Ajuste com métricas e capacidade do cluster. Pools grandes demais multiplicam conexões por réplica da aplicação.
Validando configuração
Não inicie sem URI válida. Consulte Variáveis de Ambiente no Node.js.
Collection tipada
type OrderDocument = {
_id: ObjectId;
customerId: ObjectId;
status: 'pending' | 'approved' | 'cancelled';
totalCents: number;
createdAt: Date;
updatedAt: Date;
version: number;
};
const orders = db.collection<OrderDocument>('orders');A tipagem ajuda no editor, mas não valida dados em runtime.
Inserindo documento
const now = new Date();
const result = await orders.insertOne({
_id: new ObjectId(),
customerId: new ObjectId(customerId),
status: 'pending',
totalCents,
createdAt: now,
updatedAt: now,
version: 1
});Use valores monetários inteiros e datas BSON.
ObjectId
function parseObjectId(value: string): ObjectId {
if (!ObjectId.isValid(value)) {
throw new InvalidIdError(value);
}
return new ObjectId(value);
}Valide na borda antes de consultar.
Buscando por ID
const order = await orders.findOne({
_id: parseObjectId(orderId)
});findOne retorna null quando não encontra.
Projection
const order = await orders.findOne(
{ _id },
{
projection: {
status: 1,
totalCents: 1,
customerId: 1
}
}
);Evite transferir documentos grandes quando poucos campos são necessários.
Atualização
const result = await orders.updateOne(
{ _id },
{
$set: {
status: 'approved',
updatedAt: new Date()
},
$inc: { version: 1 }
}
);
if (result.matchedCount === 0) {
throw new OrderNotFoundError(orderId);
}Lock otimista
const result = await orders.updateOne(
{
_id,
version: expectedVersion
},
{
$set: {
status: 'approved',
updatedAt: new Date()
},
$inc: { version: 1 }
}
);
if (result.matchedCount === 0) {
throw new OptimisticLockError(orderId);
}Consulte Lock Otimista no Node.js.
findOneAndUpdate
const updated = await orders.findOneAndUpdate(
{ _id, version: expectedVersion },
{
$set: { status: 'approved' },
$inc: { version: 1 }
},
{ returnDocument: 'after' }
);Use quando precisa do documento atualizado em uma única operação.
Exclusão
const result = await orders.deleteOne({ _id });Em domínios com auditoria, prefira status ou deletedAt.
Upsert
await settings.updateOne(
{ key: 'billing-mode' },
{
$set: {
value: 'automatic',
updatedAt: new Date()
}
},
{ upsert: true }
);Use quando “criar ou atualizar” é a semântica real.
Filtros
const cursor = orders.find({
status: 'pending',
createdAt: { $gte: createdAfter }
});Construa filtros por allowlist. Não aceite operadores MongoDB arbitrários do usuário.
NoSQL Injection
Entradas como:
{ "email": { "$ne": null } }podem alterar a query se o payload for repassado diretamente. Valide tipos e monte o filtro explicitamente.
Consulte Ajv no Node.js.
Ordenação
const SORT_FIELDS = {
createdAt: 'createdAt',
total: 'totalCents'
} as const;
cursor.sort({
[SORT_FIELDS[input.sort]]: input.direction === 'asc' ? 1 : -1,
_id: 1
});Inclua um desempate estável.
Paginação por cursor
const filter = cursorId
? { _id: { $gt: new ObjectId(cursorId) } }
: {};
const items = await orders
.find(filter)
.sort({ _id: 1 })
.limit(limit + 1)
.toArray();Consulte Paginação em APIs Node.js.
Limitando resultados
Nunca deixe limit ilimitado. Defina default e máximo:
const limit = Math.min(
Math.max(input.limit ?? 20, 1),
100
);Cursor e memória
Evite toArray() em milhões de documentos. Itere:
for await (const order of cursor) {
await processOrder(order);
}Aplique limites e backpressure no processamento.
Índices
await orders.createIndex({ customerId: 1, createdAt: -1 });
await orders.createIndex(
{ externalId: 1 },
{ unique: true }
);Índices devem acompanhar padrões de consulta e constraints.
Índice parcial
await orders.createIndex(
{ nextAttemptAt: 1 },
{
partialFilterExpression: {
status: 'retry_pending'
}
}
);Isso reduz tamanho quando apenas parte dos documentos é relevante.
Explain
const plan = await orders
.find(filter)
.explain('executionStats');Analise quantidade examinada, retornada e uso de índice.
Aggregation Pipeline
const result = await orders.aggregate([
{
$match: {
createdAt: { $gte: start, $lt: end }
}
},
{
$group: {
_id: '$status',
totalOrders: { $sum: 1 },
totalCents: { $sum: '$totalCents' }
}
},
{ $sort: { totalOrders: -1 } }
]).toArray();Coloque filtros seletivos cedo.
Lookup
$lookup faz junções entre collections, mas documentos embutidos ou read models podem ser melhores. Meça custo e cardinalidade.
Modelagem embutida
{
_id,
status,
items: [
{ productId, nameSnapshot, quantity, priceCents }
]
}Embuta quando dados são lidos e atualizados juntos e o tamanho é limitado.
Referências
Use IDs entre documentos quando ciclos de vida são independentes ou arrays podem crescer sem limite.
Limite do documento
MongoDB possui limite por documento. Não mantenha históricos ou eventos ilimitados dentro de um único agregado.
Schema validation no banco
Collections podem usar JSON Schema para proteção adicional. Validação na aplicação continua necessária.
Transações
const session = client.startSession();
try {
await session.withTransaction(async () => {
await orders.insertOne(order, { session });
await outbox.insertOne(event, { session });
});
} finally {
await session.endSession();
}Transações exigem topologia compatível. Mantenha curtas.
Unit of Work
Uma Unit of Work pode expor repositories ligados à mesma session. Consulte Unit of Work no Node.js.
Retry de transação
withTransaction pode repetir determinados erros transitórios. O callback não deve executar e-mail ou pagamento externo não idempotente.
Outbox
Salve documento e evento na mesma transação quando publicar depois. Consulte Outbox Pattern no Node.js.
Change Streams
const stream = orders.watch([
{
$match: {
operationType: 'insert'
}
}
]);
for await (const change of stream) {
await handleChange(change);
}Change streams exigem resiliência, resume token e tratamento de duplicidade.
Não substitua outbox automaticamente
Change stream observa alterações, mas o contrato de integração pode exigir transformação, versionamento e confirmação transacional explícita.
Read Concern e Write Concern
Configure garantias conforme criticidade. Valores mais fortes podem aumentar latência. Não altere defaults sem entender topologia e requisitos.
Read Preference
Leituras em secondary podem retornar dados atrasados. Não use para fluxos que precisam ler imediatamente uma escrita recém-confirmada.
Timeouts
Use limites de seleção e operação. AbortSignal pode cancelar operações suportadas pela versão do driver.
Repository Pattern
Encapsule queries e mapeamento:
class MongoOrderRepository
implements OrderRepository {
constructor(
private readonly collection: Collection<OrderDocument>
) {}
}Consulte Repository Pattern no Node.js.
Mapper
const OrderMapper = {
toDomain(doc: OrderDocument): Order {
return Order.restore({
id: OrderId.from(doc._id.toHexString()),
status: doc.status,
total: Money.create(doc.totalCents, 'BRL'),
version: doc.version
});
}
};Não deixe ObjectId e BSON vazarem para o domínio.
Migrations
MongoDB é flexível, mas documentos antigos continuam existindo. Use:
- campo
schemaVersion; - leitura compatível;
- backfill em lotes;
- índices criados com planejamento;
- remoção posterior de compatibilidade.
Backfill
Atualize em lotes por _id e monitore carga. Não faça um update massivo sem testar impacto.
Segurança
- use TLS;
- privilégio mínimo;
- credenciais por Secret;
- restrição de rede;
- rotação;
- auditoria;
- não registre URI.
Consulte Gestão de Segredos no Node.js.
Command monitoring
O driver oferece eventos de comandos. Use com cuidado para não registrar filtros e documentos sensíveis.
Observabilidade
Meça:
- tempo de aquisição;
- latência de operação;
- timeouts;
- pool em uso;
- erros por código;
- documentos examinados;
- lag de change streams.
Shutdown
await client.close();Pare de aceitar novas requisições, conclua operações e feche o client. Consulte Graceful Shutdown no Node.js.
Testes
Use uma instância real em container para validar:
- ObjectId;
- índices unique;
- agregações;
- transações;
- mapeamento;
- paginação;
- concorrência;
- change streams.
Não confie apenas em mocks
Mocks não reproduzem BSON, índices, session, topologia ou pipeline. Use integração para repositories.
Erros comuns
- Cliente por request: conexões explodem.
- Payload como filtro: NoSQL injection.
- Sem índices: scans crescem.
- toArray sem limite: memória é consumida.
- ObjectId no domínio: infraestrutura vaza.
- Documento ilimitado: limite é atingido.
- Transação longa: contenção aumenta.
- Secondary para leitura crítica: dados ficam atrasados.
Boas práticas
- Reutilize MongoClient.
- Tipifique collections.
- Valide em runtime.
- Monte filtros explicitamente.
- Crie índices por consulta.
- Use cursor e limites.
- Mapeie para domínio.
- Mantenha transações curtas.
- Proteja credenciais.
- Teste em MongoDB real.
Conclusão
Usar MongoDB no Node.js com o driver oficial fornece controle direto sobre documents, queries, índices, aggregations e transações. A conexão deve ser compartilhada e o pool configurado com base em carga real.
Com validação, filtros explícitos, índices, mappers, segurança e testes de integração, o driver oficial atende aplicações robustas sem exigir um ODM. A próxima etapa é comparar essa abordagem com Mongoose, que adiciona schemas e middleware na aplicação.




