Getting Started with MongoDB using Rust

Updated on November 21, 2023
Getting Started with MongoDB using Rust header image

Introduction

MongoDB is a NoSQL database that stores data in documents made up of field-value pairs. These are stored using BSON, a binary representation of JSON documents. MongoDB documents are equivalent to rows in a relational database table, and the document fields (key-value pairs) are similar to columns. A MongoDB document is part of a collection, and one or more collections are part of database. MongoDB has an API that supports regular CRUD operations (create, read, update, delete) along with aggregation, geospatial queries, and text search. MongoDB also provides high availability and replication. One can distribute data across multiple MongoDB servers (called Replica sets) to provide redundancy and sharding.

There are multiple client drivers that MongoDB officially supports. These include Java, Python, Node.js, PHP, Ruby, Swift, C, C++, C#, Go, and Rust.

In this article, you will learn how to use the Rust driver for MongoDB by building a simple command-line application.

MongoDB Rust driver

MongoDB Rust driver is used to interface with MongoDB from Rust applications. Although the application in this article uses the synchronous API, the Rust driver also provides an asynchronous API.

It supports both asynchronous runtime crates - tokio and async-std. tokio is the default runtime, but you can override this by choosing a runtime using the feature flags in Cargo.toml.

In addition to the asynchronous API, some of the important feature flags supported by the MongoDB Rust driver include:

  • openssl-tls: TLS connection handling is done using openssl.
  • bson-uuid-1 - Support for v1.x of the uuid crate in the public API of the re-exported bson crate.
  • bson-serde_with: Support for the serde_with crate in the public API of the re-exported bson crate.

Prerequisites

Before following the steps in this guide, you need to:

  1. Deploy a new Ubuntu 22.04 LTS Vultr cloud server.
  2. Create a non-root sudo user.
  3. Install Docker.
  4. Install a recent version of Rust.

Create a new Rust project

  1. SSH as the non-root user to the Ubuntu server you deployed in the Prerequisites section.

  2. Create a new Rust project and change into the directory:

     $ cargo new mongodb-rust
     $ cd mongodb-rust

    This will create a new Cargo package including a Cargo.toml manifest and a src/main.rs file.

  3. Replace the contents of Cargo.toml file with below:

     [package]
     name = "user-app"
     version = "0.1.0"
     edition = "2018"
    
     [dependencies.mongodb]
     version = "1.1.1"
     default-features = false
     features = ["sync"]
    
     [dependencies.serde]
     version = "1.0.118"
    
     [dependencies.bson]
     version = "1.1.0"

Start MongoDB container

On the Ubuntu server, start the MongoDB container using Docker.

$ docker run -it --rm -p 27017:27017 mongo

Once the container has started, MongoDB will be accessible over port 27017.

Connect to MongoDB with Rust driver

  1. Replace the contents of src/main.rs file with the below code:

     use mongodb::sync::Client;
     use mongodb::sync::Collection;
     use mongodb::bson::{doc, Bson};
     use serde::{Deserialize, Serialize};
    
     struct UsersManager {
         coll: Collection
     }
    
     fn main() {
         let conn_string = std::env::var_os("MONGODB_URL").expect("missing environment variable MONGODB_URL").to_str().expect("missing MONGODB_URL").to_owned();
    
         let users_db = std::env::var_os("MONGODB_DATABASE").expect("missing environment variable MONGODB_DATABASE").to_str().expect("missing MONGODB_DATABASE").to_owned();
    
         let users_collection = std::env::var_os("MONGODB_COLLECTION").expect("missing environment variable MONGODB_COLLECTION").to_str().expect("missing MONGODB_COLLECTION").to_owned();
    
         let um = UsersManager::new(conn_string,users_db.as_str(), users_collection.as_str());
     }
    
     impl UsersManager{
         fn new(conn_string: String, db_name: &str, coll_name: &str) -> Self{
             let mongo_client = Client::with_uri_str(&*conn_string).expect("failed to create client");
             println!("successfully connected to mongodb");
    
             let users_coll = mongo_client.database(db_name).collection(coll_name);    
             UsersManager{coll: users_coll}
         }
     }
  2. Build the program:

     $ cargo build

    Once the program is compiled and built, you should see an output similar to this:

     Finished dev [unoptimized + debuginfo] target(s) in 10.10s
  3. Run the program:

     $ export MONGODB_URL=mongodb://localhost:27017
     $ export MONGODB_DATABASE=users-db
     $ export MONGODB_COLLECTION=users
     $ cargo run

    If connected, you should see the following output:

     successfully connected to mongodb

Add the User struct

Add the code below to src/main.rs file:

#[derive(Serialize, Deserialize)]
struct User {
    #[serde(rename = "_id", skip_serializing_if = "Option::is_none")]
    user_id: Option<bson::oid::ObjectId>,
    #[serde(rename = "description")]
    email: String,
    status: String,
}

Add a method to create a user

Add the code below to src/main.rs file (under impl UsersManager after the new method):

fn add_user(self, email: &str) {
    let new_user = User {
        user_id: None,
        email: String::from(email),
        status: String::from("enabled"),
    };

    let user_doc = mongodb::bson::to_bson(&new_user).expect("conversion failed").as_document().expect("conversion failed").to_owned();
    
    let insert_result = self.coll.insert_one(user_doc, None).expect("failed to add user");    
    println!("inserted user with id = {}", insert_result.inserted_id);
}

Add a method to list all the users

Add the code below to src/main.rs file (under impl UsersManager after the add_user method):

fn list_users(self, status_filter: &str) {

    let mut filter = doc!{};
    if status_filter == "enabled" ||  status_filter == "disabled"{
        println!("listing '{}' users",status_filter);
        filter = doc!{"status": status_filter}
    } else if status_filter != "all" {
        panic!("invalid user status")
    }    
    let mut users = self.coll.find(filter, None).expect("failed to find users");

    while let Some(result) = users.next() {
        let user_doc = result.expect("user not present");

        let user: User = bson::from_bson(Bson::Document(user_doc)).expect("conversion failed");

        println!("user_id: {} \nemail: {} \nstatus: {}\n=========", user.user_id.expect("user id missing"), user.email, user.status);
    }
}

Add method to update user status

Add the code below to src/main.rs file (under impl UsersManager after the list_users method):

fn update_user(self, user_id: &str, status: &str) {

    if status != "enabled" && status != "disabled" {
        panic!("invalid user status")
    }

    println!("updating user {} status to {}", user_id, status);

    let id_filter = doc! {"_id": bson::oid::ObjectId::with_string(user_id).expect("user_id is not valid ObjectID")};

    let r = self.coll.update_one(id_filter, doc! {"$set": { "status": status }}, None).expect("user update failed");

    if r.modified_count == 1 {
        println!("updated status for user id {}",user_id);
    } else if r.matched_count == 0 {
        println!("could not update. check user id {}",user_id);
    }
}

Add a method to delete a user

Add the code below to src/main.rs file (under impl UsersManager after the update_user method):

fn delete_user(self, user_id: &str) {
    let id_filter = doc! {"_id": bson::oid::ObjectId::with_string(user_id).expect("user_id is not valid ObjectID")};
    self.coll.delete_one(id_filter, None).expect("delete failed").deleted_count;
    
    println!("deleted user {}", user_id);
}

Add command-line operations

Add the code below to the main function in src/main.rs file:

let ops: Vec<String> = std::env::args().collect();
let operation = ops[1].as_str();

match operation {
    "create" => um.add_user(ops[2].as_str()),
    "list" => um.list_users(ops[2].as_str()),
    "update" => um.update_user(ops[2].as_str(), ops[3].as_str()),
    "delete" => um.delete_user(ops[2].as_str()),
    _ => panic!("invalid user operation specified")
}

Test the application

  1. Re-build the program:

     $ cargo build --release

    You should see output similar to this (output has been redacted for brevity):

     .....
     Compiling bson v1.2.4
     Compiling mongodb v1.2.5
     Compiling user-app v0.1.0 (/Users/demo/mongodb-rust)
     Finished release [optimized] target(s) in 37.84s
  2. Change to the directory where the application binary is present:

     $ cd target/release

You can now test the CRUD (Create, Read, Update, Delete) operations supported by the command-line application.

Create users

$ ./user-app create "user1@foo.com"
$ ./user-app create "user2@foo.com"
$ ./user-app create "user3@foo.com"

If successful, for each user that was created, you should see an output with the MongoDB _id of the newly created user:

inserted user with id = ObjectId("63b5648cbd0aa2dad409a3d7")

Note that the object ID might be different in your case.

List all the users

$ ./user-app list all

You should see the users that you added in the previous step.

user_id: 63b5648cbd0aa2dad409a3d7 
email: user1@foo.com 
status: enabled
=========
user_id: 63b564c5f921fd0a9d0ea7ff 
email: user2@foo.com 
status: enabled
=========
user_id: 63b564c67f335133190fd1e2 
email: user3@foo.com 
status: enabled
=========

Note that the user (object) IDs might be different in your case.

Update the user status

Specify the user ID of the user whose status you want to update, along with the status (enabled or disabled):

$ ./user-app update 63b5648cbd0aa2dad409a3d7 disabled

Note that the user (object) ID might be different in your case. Use the one in your database.

You should see an output similar to this:

updating user 63b5648cbd0aa2dad409a3d7 status to disabled
updated status for user id 63b5648cbd0aa2dad409a3d7

List users based on their status

To fetch enabled users:

$ ./user-app list enabled

You should see an output similar to this:

listing 'enabled' users
user_id: 63b564c5f921fd0a9d0ea7ff 
email: user2@foo.com 
status: enabled
=========
user_id: 63b564c67f335133190fd1e2 
email: user3@foo.com 
status: enabled
=========

Note that the user (object) IDs might be different in your case.

To fetch disabled users:

$ ./user-app list disabled

You should see an output similar to this:

listing 'disabled' users
user_id: 63b5648cbd0aa2dad409a3d7 
email: user1@foo.com 
status: disabled
=========

Note that the user (object) ID might be different in your case.

Delete a user

Specify the user ID of the user who you want to delete.

$ ./user-app delete 63b5648cbd0aa2dad409a3d7

Note that the user (object) ID might be different in your case. Use the one in your database.

You should see an output similar to this:

deleted user 63b5648cbd0aa2dad409a3d7

Note that the user (object) ID might be different in your case.

List all the users to confirm

$ ./user-app list all

You should see an output similar to this. The user you deleted before will not be present.

user_id: 63b564c5f921fd0a9d0ea7ff 
email: user2@foo.com 
status: enabled
=========
user_id: 63b564c67f335133190fd1e2 
email: user3@foo.com 
status: enabled
=========

Note that the user (object) ID might be different in your case.

Conclusion

In this article, you started a MongoDB instance using Docker and interacted with it using a command-line application written in Rust.

You can also learn more in the following documentation: