---
title: "MongoDB com Rust: CRUD com o driver oficial | Rust Brasil"
url: "https://rustlang.com.br/blog/rust-mongodb-crud-atlas-driver-2026/"
markdown_url: "https://rustlang.com.br/blog/rust-mongodb-crud-atlas-driver-2026.MD"
description: "Use MongoDB com Rust pelo driver oficial: conexão, CRUD com serde, ObjectId, índices, transações, pool de conexões e integração com Axum em produção."
date: "2026-10-05"
author: "Equipe Rust Brasil"
---

# MongoDB com Rust: CRUD com o driver oficial | Rust Brasil

Use MongoDB com Rust pelo driver oficial: conexão, CRUD com serde, ObjectId, índices, transações, pool de conexões e integração com Axum em produção.


**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](/blog/rust-banco-dados-sqlx-diesel-seaorm-2026/).

## Resposta rápida: quando MongoDB com Rust?

| Situação | Escolha | Por quê |
|---|---|---|
| Dados relacionais, integridade referencial | **PostgreSQL + [SQLx](/ecossistema/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](/blog/rust-redis-cache-backend-2026/)** | expiração nativa e latência de memória |
| Dados embarcados em CLI/edge | **SQLite via [rusqlite](/ecossistema/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`:

```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:

```bash
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:

```rust
use mongodb::{Client, Database};

async fn connect() -> anyhow::Result<Database> {
    // Local: mongodb://localhost:27017
    // Atlas:  mongodb+srv://usuario:senha@cluster0.xxxxx.mongodb.net
    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](/ecossistema/serde/) — o mesmo ecossistema do resto do backend Rust:

```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_if` deixa o banco gerar o `ObjectId` no insert — nunca gere IDs no cliente sem necessidade.

## CRUD completo

```rust
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!`:

```rust
let filtro = doc! {
    "status": "pago",
    "preco_em_centavos": { "$gte": 10_000 },
};
```

Para relatórios, o pipeline de agregação roda com `aggregate`:

```rust
// 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:

```rust
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:

```rust
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](/tutoriais/api-rest-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:

```rust
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 `Client` no 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.
- [ ] `ObjectId` no 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](/artigos/logging-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](/tutoriais/api-rest-axum/) usando este driver como persistência.
- Compare com o caminho relacional no [guia SQLx, Diesel e SeaORM](/blog/rust-banco-dados-sqlx-diesel-seaorm-2026/).
- Veja onde Mongo e Rust aparecem no mercado no [guia de carreira web backend](/carreira/nicho-web-backend/) e nas [vagas de Rust](/vagas/).
