A forma oficial de usar MongoDB com Rust é o crate mongodb: um driver assíncrono mantido pela própria MongoDB, que roda sobre o Tokio e fala bson e serde nativamente. Um CRUD completo cabe em poucas linhas — insert_one, find, update_one, delete_one — com structs Rust serializadas por serde e o _id como ObjectId. O Client mantém pool de conexões interno e se compartilha entre handlers de Axum sem custo. Este guia mostra conexão local e no Atlas, CRUD completo, mapeamento do _id, índices, transações e o checklist de produção — a contraparte NoSQL do nosso guia de bancos de dados com SQLx, Diesel e SeaORM.
Resposta rápida: quando MongoDB com Rust?
| Situação | Escolha | Por quê |
|---|---|---|
| Dados relacionais, integridade referencial | PostgreSQL + SQLx | JOINs, constraints e SQL maduro |
| Schema variável por documento, catálogo, eventos | MongoDB | documentos flexíveis sem migrations de schema |
| Alto volume de escrita com leitura por chave | MongoDB | escritas simples e índices fortes |
| Cache de resposta com TTL | Redis | expiração nativa e latência de memória |
| Dados embarcados em CLI/edge | SQLite via rusqlite | um arquivo, sem servidor |
| API REST backend padrão em equipe | PostgreSQL | contratação e ferramentas no Brasil |
MongoDB brilha quando a unidade natural de dados é o documento agregado — um pedido com itens, um perfil com permissões, um evento com payload heterogêneo. Se você se pega desmontando o documento em cinco tabelas, use SQL.
Setup: dependências e conexão
Adicione ao Cargo.toml:
[dependencies]
mongodb = "3"
serde = { version = "1", features = ["derive"] }
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
Para desenvolvimento local, suba um Mongo com Docker em uma linha:
docker run -d --name mongo -p 27017:27017 mongo:7
A conexão usa ClientOptions::parse, que já resolve a topologia e as opções da string:
use mongodb::{Client, Database};
async fn connect() -> anyhow::Result<Database> {
// Local: mongodb://localhost:27017
// Atlas: mongodb+srv://usuario:[email protected]
let uri = std::env::var("MONGO_URI")
.unwrap_or_else(|_| "mongodb://localhost:27017".into());
let client = Client::with_options(
mongodb::ClientOptions::parse(&uri).await?
)?;
Ok(client.database("app"))
}
Carregue a senha de variável de ambiente ou secret manager — a string de conexão com credencial não vai no repositório.
O modelo: struct com serde e o _id
O driver serializa structs com serde — o mesmo ecossistema do resto do backend Rust:
use bson::oid::ObjectId;
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
pub struct Pedido {
#[serde(rename = "_id", skip_serializing_if = "Option::is_none")]
pub id: Option<ObjectId>,
pub cliente: String,
pub itens: Vec<Item>,
pub status: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Item {
pub sku: String,
pub quantidade: u32,
pub preco_em_centavos: u64,
}
Dois detalhes que economizam horas:
rename = "_id"faz o campo conversar com o identificador real do MongoDB;Option+skip_serializing_ifdeixa o banco gerar oObjectIdno insert — nunca gere IDs no cliente sem necessidade.
CRUD completo
use bson::doc;
use mongodb::{Collection, results::*};
impl Pedido {
fn coll(db: &Database) -> Collection<Pedido> {
db.collection("pedidos")
}
}
// CREATE
async fn criar(db: &Database, p: Pedido) -> anyhow::Result<ObjectId> {
let res: InsertOneResult = Pedido::coll(db).insert_one(p).await?;
Ok(res.inserted_id.as_object_id().unwrap())
}
// READ (um)
async fn buscar(db: &Database, id: ObjectId) -> anyhow::Result<Option<Pedido>> {
Ok(Pedido::coll(db)
.find_one(doc! { "_id": id })
.await?)
}
// READ (lista com filtro)
async fn listar_do_cliente(db: &Database, cliente: &str) -> anyhow::Result<Vec<Pedido>> {
use futures_util::StreamExt;
let mut cursor = Pedido::coll(db)
.find(doc! { "cliente": cliente })
.await?;
let mut out = Vec::new();
while let Some(p) = cursor.next().await {
out.push(p?);
}
Ok(out)
}
// UPDATE (campo único, atômico)
async fn avancar_status(db: &Database, id: ObjectId) -> anyhow::Result<bool> {
let res: UpdateResult = Pedido::coll(db)
.update_one(
doc! { "_id": id, "status": "novo" },
doc! { "$set": { "status": "pago" } },
)
.await?;
Ok(res.modified_count == 1)
}
// DELETE
async fn remover(db: &Database, id: ObjectId) -> anyhow::Result<bool> {
let res: DeleteResult = Pedido::coll(db)
.delete_one(doc! { "_id": id })
.await?;
Ok(res.deleted_count == 1)
}
Repare no update: o filtro inclui "status": "novo". Isso transforma a mudança de status em operação atômica — dois handlers concorrentes não avançam o mesmo pedido duas vezes, sem lock aplicativo.
Filtro dinâmico e agregação
Filtros compostos sobem com doc!:
let filtro = doc! {
"status": "pago",
"preco_em_centavos": { "$gte": 10_000 },
};
Para relatórios, o pipeline de agregação roda com aggregate:
// Cada estágio é um Document; o pipeline é um Vec<Document>
let pipeline = vec![
doc! {
"$match": { "status": "pago" }
},
doc! {
"$group": {
"_id": "$cliente",
"total": { "$sum": "$preco_em_centavos" },
"pedidos": { "$sum": 1 },
}
},
doc! { "$sort": { "total": -1 } },
doc! { "$limit": 10 },
];
Índices: a diferença entre rápido e intolerável
Todo campo de busca frequente precisa de índice — find sem índice é varredura completa:
use mongodb::{IndexModel, bson::Document};
async fn criar_indices(db: &Database) -> anyhow::Result<()> {
let idx = IndexModel::builder()
.keys(doc! { "cliente": 1, "status": 1 })
.build();
Pedido::coll(db).create_index(idx).await?;
Ok(())
}
Rode a criação de índices no startup (idempotente) e confirme com .explain() que as queries do caminho quente usam índice.
Integração com Axum
O Client é barato de clonar (um Arc interno) e mantém o pool de conexões — o padrão é colocá-lo no estado da aplicação:
use axum::{extract::State, routing::get, Router, Json};
use std::sync::Arc;
#[derive(Clone)]
struct AppState {
mongo: Arc<mongodb::Client>,
db: String,
}
async fn pedido_por_id(
State(state): State<AppState>,
axum::extract::Path(id): axum::extract::Path<String>,
) -> Result<Json<Pedido>, StatusCode> {
let oid = ObjectId::parse_str(&id)
.map_err(|_| StatusCode::BAD_REQUEST)?;
let pedido = Pedido::coll(&state.mongo.database(&state.db))
.find_one(doc! { "_id": oid })
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
.ok_or(StatusCode::NOT_FOUND)?;
Ok(Json(pedido))
}
O mesmo desenho de estado do nosso tutorial de API REST com Axum — só trocando a fonte de dados.
Transações multi-documento
Transações exigem replica set (o Atlas atende por padrão) e uma sessão:
async fn transferir_estoque(
db: &Database,
pedido_id: ObjectId,
) -> anyhow::Result<()> {
let mut session = db.client().start_session().await?;
session
.with_transaction(|sess| async move {
// update_one com &mut session em cada operação...
Ok(())
})
.await?;
Ok(())
}
with_transaction repete a transação automaticamente quando o MongoDB devolve um erro transitório. Antes de adotar transações, verifique se uma atualização atômica em documento único não resolve — na maioria dos casos, resolve.
Checklist de produção
- Um único
Clientno startup, compartilhado via estado — nunca por requisição. - Connection string em variável de ambiente ou secret manager.
- Índices criados no startup para todos os campos de filtro.
-
ObjectIdno modelo; conversão para string só na borda da API. - Timeouts configurados no
ClientOptions(o padrão é 30 s por operação). -
explain()nas queries do caminho quente antes do go-live. - Monitoramento do pool e de erros de conexão no seu stack de observabilidade.
Erros comuns
Criar Client a cada request. Cada construção abre conexões TLS e redescobre a topologia. Compartilhe um.
Serializar _id como String. Guarde ObjectId no modelo e converta na borda — armazenar string quebra a ordenação natural do ObjectId (que carrega timestamp) e dobra o tamanho do índice.
Ignorar a concorrência no update. Ler o documento, mudar em Rust e gravar de volta abre condição de corrida. Prefira $set com filtro de pré-condição, como no exemplo de status.
Esperar transação em standalone. Instância única sem replica set não suporta transações multi-documento; ajuste o ambiente ou redesenhe para documento único.
Próximos passos
- Monte uma API completa no tutorial de Axum usando este driver como persistência.
- Compare com o caminho relacional no guia SQLx, Diesel e SeaORM.
- Veja onde Mongo e Rust aparecem no mercado no guia de carreira web backend e nas vagas de Rust.