Skip to Content
Course content

178: MongoDB Driver Usage in Rust

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I once worked with a developer who had just migrated a Node.js backend to Rust. They were flying through the logic, but they hit a wall the moment they touched MongoDB. They spent an entire afternoon fighting the compiler because they were trying to treat MongoDB's ObjectId as a simple String. In JavaScript, the driver often masks these distinctions, but Rust demands you be explicit about whether you're dealing with a BSON document, a raw string, or a typed struct. It's a common point of frustration, but once you understand how the mongodb crate leverages serde, it actually becomes a superpower for data integrity.

Taming the BSON-Serde Bridge

The key to working with MongoDB in Rust is understanding that the driver doesn't just "send data"β€”it translates between Rust types and BSON (Binary JSON). To make this painless, you'll almost always use serde. Instead of manually building BSON documents for everything, you define your data models as structs and derive Serialize and Deserialize.

Here is where the ObjectId trip-up happens. If you want your Rust struct to map directly to the _id field in Mongo, you can't just use a String. You should use the oid type from the bson crate. I usually recommend using the #[serde(rename = "_id", skip_serializing_if = "Option::is_none")] attribute on an Option<ObjectId> field. This allows MongoDB to generate the ID for you on insertion while still letting you access it when you read the document back.

use serde::{Serialize, Deserialize};
use mongodb::bson::oid::ObjectId;

#[derive(Debug, Serialize, Deserialize)]
pub struct DeviceLog {
    #[serde(rename = "_id", skip_serializing_if = "Option::is_none")]
    pub id: Option<ObjectId>,
    pub device_id: String,
    pub temperature: f64,
    pub timestamp: mongodb::bson::DateTime,
}

Executing Async Queries with the doc! Macro

You'll notice the mongodb crate is heavily async, typically relying on tokio. When you're querying, you aren't writing SQL; you're building BSON documents. The driver provides a doc! macro that makes this look and feel like JSON, which saves you from the verbosity of manually constructing BSON maps.

When I'm writing query logic, I prefer to keep my collection handles typed. Instead of using a generic Collection<Document>, use Collection<DeviceLog>. This forces the driver to handle the deserialization for you, so you get a stream of your actual structs rather than a bunch of loosely typed BSON maps that you have to manually cast.

use mongodb::{Client, Collection};
use mongodb::bson::doc;
use futures::stream::TryStreamExt;

async fn get_high_temp_logs(coll: &Collection<DeviceLog>, threshold: f64) -> Vec<DeviceLog> {
    let filter = doc! { "temperature": { "$gt": threshold } };
    let mut cursor = coll.find(filter, None).await.expect("Query failed");
    
    let mut results = Vec::new();
    while let Some(log) = cursor.try_next().await.unwrap() {
        results.push(log);
    }
    results
}

Managing the MongoClient Lifecycle

One thing that catches people off guard is the Client. You might be tempted to create a new client every time you need to perform a database operation, but don't do that. The Client in the Rust driver is internally wrapped in an Arc, meaning it's designed to be cloned and shared across your entire application.

I always suggest initializing the Client once at the start of your main function and passing clones of it into your request handlers or services. This ensures you're utilizing connection pooling effectively. If you create a new client for every request, you'll quickly exhaust your database's connection limit and see your latency spike as the driver spends more time performing TCP handshakes than actually moving data.




πŸ“‹ Practical Task

Implementing a Device Heartbeat Log Store

Build a small module that manages "heartbeat" logs for a fleet of IoT devices. Your implementation must satisfy the following requirements:

  • Define a Heartbeat struct that includes an _id (as an optional ObjectId), a device_id (String), and a status (String). Use serde to ensure the _id maps correctly to MongoDB.
  • Write an async function record_heartbeat that takes a Collection<Heartbeat> and a Heartbeat instance, inserting the document into the database.
  • Write an async function find_device_status that takes a Collection<Heartbeat> and a device_id, returning the single most recent Heartbeat document for that device (Hint: use a sort on the _id field in descending order, as ObjectIds contain a timestamp).
  • Ensure your functions handle Result types properly rather than using unwrap() inside the logic.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.