Rust and MongoDB Tutorial: Create CRUD REST-API

by Didin J. on Dec 01, 2024 Rust and MongoDB Tutorial: Create CRUD REST-API

A quick and comprehensive way to create CRUD REST-API using Rust, Actix Web, and MongoDB is explained in step-by-step

There are several steps to creating, reading, updating, and deleting (CRUD) REST-API using Rust, MongoDB, and Actix Web. In this tutorial, we will create Person and Hobby models or entities related by one-to-many relationships, which in MongoDB can be achieved by adding Person references to the Hobby collection. At the end, we will have these REST-API endpoints.

POST /api/v1/persons
GET /api/v1/persons
GET /api/v1/persons/:id
PUT /api/v1/persons
DELETE /api/v1/persons
POST /api/v1/hobbies
GET /api/v1/hobbies
GET /api/v1/hobbies/:id
PUT /api/v1/hobbies
DELETE /api/v1/hobbies

Creating the REST-API with Rust and MongoDB requires several steps:

Before we start, the following tools, framework, library, and dependencies are required:

  1. Rust/Cargo CLI
  2. MongoDB
  3. IDE (We will use VSCode)
  4. Terminal or Command Line

Let's get started with the first step!


Step 1. Preparation

1. Setup MongoDB

First, we need to install MongoDB Community. We can download the HomeBrew formula for MongoDB on Mac and install it.

brew tap mongodb/brew
brew install [email protected]

To view the existing HomeBrew services, type this command.

brew services list

You can see the installed MongoDB Community like below.

Name                  Status User File
[email protected] none 

Start, Stop, or Restart using HomeBrew like this.

brew services start [email protected]

Make sure the MongoDB is running like this.

brew services list                       
Name                  Status  User  File
[email protected] started didin ~/Library/LaunchAgents/[email protected]

Next, create a user for the database that Rust REST-API will use. Go to the MongoDB shell by typing this command.

mongosh

Use the Admin DB.

use admin

Type this command to create a new user for the specific database.

db.createUser( { user: "rustadmin", pwd: "Adm1n_Pwd", roles: ["readWrite", "dbAdmin"] } );

Next, turn on the security in the MongoDB config.

nano /opt/homebrew/etc/mongod.conf 

Remove the # from these lines.

security:
  authorization: "enabled"

Restart the MongoDB service.

brew services restart [email protected]

2. Setup Rust CLI

Next, we will install the Rust Lang by downloading the Rust Setup file.

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

Then follow the on-screen instructions. Type these commands to find the version of the installed Rustup and Cargo.

rustup -V
cargo -V

3. Prepare the VSCode

After downloading https://go.microsoft.com/fwlink/?LinkID=534106 and installing the Visual Studio Code, start the application.

code .

Go to the Extension menu on the left toolbar. Search the extension marketplace with the keyword "Rust". Install the following Rust extensions.

rust
rust-analyzer
Rust Syntax
Rust Extension Pack
Rust Doc Viewer
Prettier - Code Formatter

Now, we are ready to develop a REST API using Rust and MongoDB.


Step 2. Create A Rust REST-API Application

After the Cargo is ready, type this command to create or initialize a new Rust application.

cargo new rust_mongo_rest

Go to the newly created Rust application folder.

cd rust_mongo_rest

Open it using your IDE, for VSCode type this command.

code .

The Rust project structure will look like this.

Rust and MongoDB Tutorial: Create CRUD REST-API - structure

To add the required dependencies, open the "Cargo.toml" file from the project's root and add these dependencies.

[dependencies]
actix-web = "4"
mongodb = "3.1.0"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
dotenv = "0.15"
futures = "0.3"
tokio = { version = "1", features = ["full"] }
chrono = { version = "*", features = ["serde"] }

Save and wait for minutes while the IDE downloads the dependencies. Now, this Rust project is ready to develop.


Step 3. Connect to MongoDB

The next step is creating a connection to MongoDB which we have installed and configured in the first step. Create a .env file at the root project folder for MongoDB URL, username, password, database name, and database authentication source.

touch .env

Fill that file with these configuration keys and values.

DB_USERNAME=rustadmin
DB_PASSWORD=Adm1n_Pwd
DATABASE_URL=mongodb://localhost:27017
DATABASE_NAME=rust_mongo
DB_AUTH_SOURCE=admin

Next, create a new Rust file in the src folder to implement a connection to MongoDB.

touch src/db.rs

Add these lines of Rust codes to that file.

use mongodb::{ options::{ ClientOptions, Credential }, Client };
use std::env;

pub async fn get_mongo_client() -> Client {
    // Load environment variables
    dotenv::dotenv().ok();

    let username = env::var("DB_USERNAME").expect("DB_USERNAME must be set");
    let password = env::var("DB_PASSWORD").expect("DB_PASSWORD must be set");
    let host = env::var("DB_HOST").unwrap_or_else(|_| "localhost".to_string());
    let port = env::var("DB_PORT").unwrap_or_else(|_| "27017".to_string());
    let auth_source = env::var("DB_AUTH_SOURCE").unwrap_or_else(|_| "admin".to_string());

    // Build the credential options
    let credential = Credential::builder()
        .username(Some(username))
        .password(Some(password))
        .source(Some(auth_source))
        .build();

    // Build the client options
    let client_uri = format!("mongodb://{}:{}", host, port);
    let mut options = ClientOptions::parse(client_uri).await.unwrap();
    options.credential = Some(credential);

    Client::with_options(options).unwrap()
}

The two first lines of the code are imports of the required libraries that will be used by the method or function. The next line is the public and asynchronous function or method. This method will read the previously created ".env" file by the "dotenv" function and then build the credentials that include username, password, and authentication source. So, the Rust application will connect to the MongoDB using URL and credentials.


Step 4. Create Rust Models

As mentioned in the first paragraph, we will create a Person and Hobby collection. So, we will create two models in the models folder.

mkdir src/models
touch src/models/person.rs
touch src/models/hobby.rs
touch src/models/mod.rs

Open the "person.rs" file then add these lines of Rust codes.

use mongodb::bson::oid::ObjectId;
use serde::{ Deserialize, Serialize };
use chrono::Utc;
use crate::models::utils::{
    deserialize_datetime,
    deserialize_object_id,
    serialize_datetime,
    serialize_object_id,
};

#[derive(Debug, Serialize, Deserialize)]
pub struct Person {
    #[serde(
        rename = "_id",
        skip_serializing_if = "Option::is_none",
        serialize_with = "serialize_object_id",
        deserialize_with = "deserialize_object_id"
    )]
    pub id: Option<ObjectId>,

    #[serde(rename = "name")]
    pub name: String,

    #[serde(rename = "email")]
    pub email: String,

    #[serde(rename = "phone")]
    pub phone: String,

    #[serde(
        rename = "created_at",
        serialize_with = "serialize_datetime",
        deserialize_with = "deserialize_datetime"
    )]
    pub created_at: chrono::DateTime<Utc>,
}

#[derive(Debug, Deserialize)]
pub struct CreatePerson {
    #[serde(rename = "name")]
    pub name: String,

    #[serde(rename = "email")]
    pub email: String,

    #[serde(rename = "phone")]
    pub phone: String,

    #[serde(
        rename = "created_at",
        serialize_with = "serialize_datetime",
        deserialize_with = "deserialize_datetime"
    )]
    pub created_at: chrono::DateTime<Utc>,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct ListPerson {
    #[serde(rename = "name")]
    pub name: String,

    #[serde(rename = "email")]
    pub email: String,
}

Open the "hobby.rs" file then add these lines of Rust codes.

use mongodb::bson::oid::ObjectId;
use serde::{ Deserialize, Serialize };
use chrono::Utc;
use crate::models::utils::{
    deserialize_datetime,
    deserialize_object_id,
    serialize_datetime,
    serialize_object_id,
};

#[derive(Debug, Serialize, Deserialize)]
pub struct Hobby {
    #[serde(
        rename = "_id",
        skip_serializing_if = "Option::is_none",
        serialize_with = "serialize_object_id",
        deserialize_with = "deserialize_object_id"
    )]
    pub id: Option<ObjectId>,

    #[serde(rename = "hobby_name")]
    pub hobby_name: String,

    #[serde(rename = "hobby_description")]
    pub hobby_description: String,

    #[serde(
        rename = "created_at",
        serialize_with = "serialize_datetime",
        deserialize_with = "deserialize_datetime"
    )]
    pub created_at: chrono::DateTime<Utc>,

    #[serde(
        rename = "person",
        skip_serializing_if = "Option::is_none",
        serialize_with = "serialize_object_id",
        deserialize_with = "deserialize_object_id"
    )]
    pub person: Option<ObjectId>,
}

#[derive(Debug, Deserialize)]
pub struct CreateHobby {
    #[serde(rename = "hobby_name")]
    pub hobby_name: String,

    #[serde(rename = "hobby_description")]
    pub hobby_description: String,

    #[serde(
        rename = "person",
        skip_serializing_if = "Option::is_none",
        serialize_with = "serialize_object_id",
        deserialize_with = "deserialize_object_id"
    )]
    pub person: Option<ObjectId>,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct ListHobby {
    #[serde(rename = "hobby_name")]
    pub hobby_name: String,
}

The imports on those files are the required library or functions that will be used to create a data structure for the models. They are MongoDB ObjectId, Deserialize, and Serialize from/to JSON, UTC Datetime using Chrono dependency, and the function to serialize/deserialize DateTime and ObjectId that will be created later. 

Each of them has 3 data structures which first data structure representing the whole MongoDB collection. The second data structure represents the DTO (Data Transfer Object) request body for POST. The last data structure represents the DTO response for the list of persons.

The "#[derive(Debug, Serialize, Deserialize)]" means attribute allows new items to be automatically generated for data structures. The "pub struct" means this file is a data structure with a public modifier. The "#[serde(...)]" are the mappings to the MongoDB collections. The implementation or one-to-many relation between a Person and a Hobby is described as the "pub person".

Next, create a new file for the Serializer and Deserializer that have been used in the models. 

touch src/models/utils.rs

Open that file then add these lines of Rust codes.

use chrono::{DateTime, Utc};
use mongodb::bson::oid::ObjectId;
use mongodb::bson::DateTime as BsonDateTime;
use serde::de::Deserializer;
use serde::ser::Serializer;
use serde::Deserialize;

// Deserialize MongoDB's DateTime or an RFC3339 string into Chrono's DateTime<Utc>
pub fn deserialize_datetime<'de, D>(deserializer: D) -> Result<DateTime<Utc>, D::Error>
where
    D: serde::Deserializer<'de>,
{
    struct DateTimeVisitor;

    impl<'de> serde::de::Visitor<'de> for DateTimeVisitor {
        type Value = DateTime<Utc>;

        fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
            formatter.write_str("a valid BSON datetime or an RFC3339 formatted string")
        }

        fn visit_map<A>(self, map: A) -> Result<Self::Value, A::Error>
        where
            A: serde::de::MapAccess<'de>,
        {
            let bson_datetime: BsonDateTime =
                serde::Deserialize::deserialize(serde::de::value::MapAccessDeserializer::new(map))?;
            Ok(bson_to_chrono(bson_datetime))
        }

        fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
        where
            E: serde::de::Error,
        {
            DateTime::parse_from_rfc3339(value)
                .map(|dt| dt.with_timezone(&Utc))
                .map_err(|_| E::invalid_value(serde::de::Unexpected::Str(value), &self))
        }
    }

    deserializer.deserialize_any(DateTimeVisitor)
}

// Serialize Chrono's DateTime<Utc> into a BSON-compatible format or string
pub fn serialize_datetime<S>(date: &DateTime<Utc>, serializer: S) -> Result<S::Ok, S::Error>
where
    S: Serializer,
{
    serializer.serialize_str(&date.to_rfc3339())
}

/// Converts a `mongodb::bson::DateTime` to `chrono::DateTime<Utc>`.
pub fn bson_to_chrono(bson_datetime: BsonDateTime) -> DateTime<Utc> {
    let timestamp_millis = bson_datetime.timestamp_millis();
    let seconds = timestamp_millis / 1000;
    let nanos = (timestamp_millis % 1000) * 1_000_000;

    // Unwrap the Option to get the DateTime<Utc>
    DateTime::from_timestamp(seconds, nanos as u32)
        .expect("Invalid timestamp in BSON DateTime")
}

pub fn serialize_object_id<S>(id: &Option<ObjectId>, serializer: S) -> Result<S::Ok, S::Error>
where
    S: Serializer,
{
    match id {
        Some(ref oid) => serializer.serialize_str(&oid.to_string()),
        None => serializer.serialize_none(),
    }
}

pub fn deserialize_object_id<'de, D>(deserializer: D) -> Result<Option<ObjectId>, D::Error>
where
    D: Deserializer<'de>,
{
    Option::deserialize(deserializer).and_then(|opt| match opt {
        Some(v) => Ok(Some(v)),
        None => Ok(None),
    })
}

The imports on that file are the required libraries for serializing and deserializing ObjectId and DateTime. The first function is a function to deserialize MongoDB's DateTime to Chrono's DateTime<Utc> described by the return type "Result<DateTime<Utc>". The next function is a function to serialize Chrono's DateTime<Utc> into a BSON-compatible format or string. The next function is a function to serialize String to MongoDB's ObjectID. The last function is a function to deserialize MongoDB's ObjectID to the String.

Next, open the "mod.rs" file to register both model names as a module in the models folder then add these lines of Rust code to it.

pub mod person;
pub mod hobby;
pub mod utils;

The Person and Hobby models will be accessible outside of this folder.


Step 5. Create Rust Handlers or Controllers

We put the logic to create, read, update, and delete data (CRUD) inside the Handlers or Controllers. It wraps all Database access, request, response, and their mappings. Create a new folder and Rust files inside it.

mkdir src/handlers
touch src/handlers/persons.rs
touch src/handlers/hobbies.rs
touch src/handlers/mod.rs

Open the "persons.rs" and then add these lines of Rust codes.

use actix_web::{ web, HttpResponse, Responder };
use chrono::Utc;
use futures::StreamExt;
use mongodb::{ bson::{ doc, oid::ObjectId }, Client };
use serde_json::json;

use crate::models::person::{ Person, CreatePerson, ListPerson };

pub async fn create_person(
    db_client: web::Data<Client>,
    person: web::Json<CreatePerson>
) -> impl Responder {
    let db = db_client.database("rust_mongo");
    let collection = db.collection::<Person>("persons");
    let new_person = Person {
        id: None,
        name: person.name.clone(),
        email: person.email.clone(),
        phone: person.phone.clone(),
        created_at: Utc::now(),
    };

    let result = collection.insert_one(new_person).await;

    match result {
        Ok(insert_result) =>
            HttpResponse::Ok().json(json!({ "inserted_id": insert_result.inserted_id })),
        Err(e) => HttpResponse::InternalServerError().body(e.to_string()),
    }
}

pub async fn get_persons(db_client: web::Data<Client>) -> impl Responder {
    let db = db_client.database("rust_mongo");
    let collection = db.collection::<ListPerson>("persons");

    let cursor = collection.find(doc! {}).await;
    match cursor {
        Ok(mut results) => {
            let mut persons: Vec<ListPerson> = vec![];
            while let Some(result) = results.next().await {
                match result {
                    Ok(person) => persons.push(person),
                    Err(_) => {
                        return HttpResponse::InternalServerError().finish();
                    }
                }
            }
            HttpResponse::Ok().json(persons)
        }
        Err(_) => HttpResponse::InternalServerError().finish(),
    }
}

pub async fn get_person_by_id(
    db_client: web::Data<Client>,
    id: web::Path<String>
) -> impl Responder {
    let db = db_client.database("rust_mongo");
    let collection = db.collection::<Person>("persons");
    let object_id = match ObjectId::parse_str(&id.into_inner()) {
        Ok(oid) => oid,
        Err(_) => {
            return HttpResponse::BadRequest().body("Invalid ID format");
        }
    };

    match collection.find_one(doc! { "_id": object_id }).await {
        Ok(Some(person)) => HttpResponse::Ok().json(person),
        Ok(None) => HttpResponse::NotFound().body("Person not found"),
        Err(e) => HttpResponse::InternalServerError().body(e.to_string()),
    }
}

pub async fn update_person_by_id(
    db_client: web::Data<Client>,
    id: web::Path<String>,
    person: web::Json<CreatePerson>
) -> impl Responder {
    let db = db_client.database("rust_mongo");
    let collection = db.collection::<Person>("persons");
    let object_id = match ObjectId::parse_str(&id.into_inner()) {
        Ok(oid) => oid,
        Err(_) => {
            return HttpResponse::BadRequest().body("Invalid ID format");
        }
    };

    let update_doc =
        doc! {
        "$set": {
            "name": &person.name,
            "email": &person.email,
            "phone": &person.phone,
        }
    };

    match collection.update_one(doc! { "_id": object_id }, update_doc).await {
        Ok(update_result) if update_result.matched_count > 0 =>
            HttpResponse::Ok().body("Person updated"),
        Ok(_) => HttpResponse::NotFound().body("Person not found"),
        Err(e) => HttpResponse::InternalServerError().body(e.to_string()),
    }
}

pub async fn delete_person_by_id(
    db_client: web::Data<Client>,
    id: web::Path<String>
) -> impl Responder {
    let db = db_client.database("rust_mongo");
    let collection = db.collection::<Person>("persons");
    let object_id = match ObjectId::parse_str(&id.into_inner()) {
        Ok(oid) => oid,
        Err(_) => {
            return HttpResponse::BadRequest().body("Invalid ID format");
        }
    };

    match collection.delete_one(doc! { "_id": object_id }).await {
        Ok(delete_result) if delete_result.deleted_count > 0 =>
            HttpResponse::Ok().body("Person deleted"),
        Ok(_) => HttpResponse::NotFound().body("Person not found"),
        Err(e) => HttpResponse::InternalServerError().body(e.to_string()),
    }
}

Open the "hobbies.rs" and then add these lines of Rust codes.

use actix_web::{ web, HttpResponse, Responder };
use chrono::Utc;
use futures::StreamExt;
use mongodb::{ bson::{ doc, oid::ObjectId }, Client };
use serde_json::json;

use crate::models::hobby::{ Hobby, CreateHobby, ListHobby };

pub async fn create_hobby(
    db_client: web::Data<Client>,
    hobby: web::Json<CreateHobby>
) -> impl Responder {
    let db = db_client.database("rust_mongo");
    let collection = db.collection::<Hobby>("hobbies");
    let new_hobby = Hobby {
        id: None,
        hobby_name: hobby.hobby_name.clone(),
        hobby_description: hobby.hobby_description.clone(),
        created_at: Utc::now(),
        person: hobby.person,
    };

    let result = collection.insert_one(new_hobby).await;

    match result {
        Ok(insert_result) =>
            HttpResponse::Ok().json(json!({ "inserted_id": insert_result.inserted_id })),
        Err(e) => HttpResponse::InternalServerError().body(e.to_string()),
    }
}

pub async fn get_hobbies(db_client: web::Data<Client>) -> impl Responder {
    let db = db_client.database("rust_mongo");
    let collection = db.collection::<ListHobby>("hobbies");

    let cursor = collection.find(doc! {}).await;
    match cursor {
        Ok(mut results) => {
            let mut hobbies: Vec<ListHobby> = vec![];
            while let Some(result) = results.next().await {
                match result {
                    Ok(hobby) => hobbies.push(hobby),
                    Err(_) => {
                        return HttpResponse::InternalServerError().finish();
                    }
                }
            }
            HttpResponse::Ok().json(hobbies)
        }
        Err(_) => HttpResponse::InternalServerError().finish(),
    }
}

pub async fn get_hobby_by_id(
    db_client: web::Data<Client>,
    id: web::Path<String>
) -> impl Responder {
    let db = db_client.database("rust_mongo");
    let collection = db.collection::<Hobby>("hobbies");
    let object_id = match ObjectId::parse_str(&id.into_inner()) {
        Ok(oid) => oid,
        Err(_) => {
            return HttpResponse::BadRequest().body("Invalid ID format");
        }
    };

    match collection.find_one(doc! { "_id": object_id }).await {
        Ok(Some(hobby)) => HttpResponse::Ok().json(hobby),
        Ok(None) => HttpResponse::NotFound().body("Hobby not found"),
        Err(e) => HttpResponse::InternalServerError().body(e.to_string()),
    }
}

pub async fn update_hobby_by_id(
    db_client: web::Data<Client>,
    id: web::Path<String>,
    hobby: web::Json<CreateHobby>
) -> impl Responder {
    let db = db_client.database("rust_mongo");
    let collection = db.collection::<Hobby>("hobbies");
    let object_id = match ObjectId::parse_str(&id.into_inner()) {
        Ok(oid) => oid,
        Err(_) => {
            return HttpResponse::BadRequest().body("Invalid ID format");
        }
    };

    let update_doc =
        doc! {
        "$set": {
            "hobby_name": &hobby.hobby_name,
            "hobby_description": &hobby.hobby_description,
        }
    };

    match collection.update_one(doc! { "_id": object_id }, update_doc).await {
        Ok(update_result) if update_result.matched_count > 0 =>
            HttpResponse::Ok().body("Hobby updated"),
        Ok(_) => HttpResponse::NotFound().body("Hobby not found"),
        Err(e) => HttpResponse::InternalServerError().body(e.to_string()),
    }
}

pub async fn delete_hobby_by_id(
    db_client: web::Data<Client>,
    id: web::Path<String>
) -> impl Responder {
    let db = db_client.database("rust_mongo");
    let collection = db.collection::<Hobby>("hobbies");
    let object_id = match ObjectId::parse_str(&id.into_inner()) {
        Ok(oid) => oid,
        Err(_) => {
            return HttpResponse::BadRequest().body("Invalid ID format");
        }
    };

    match collection.delete_one(doc! { "_id": object_id }).await {
        Ok(delete_result) if delete_result.deleted_count > 0 =>
            HttpResponse::Ok().body("Hobby deleted"),
        Ok(_) => HttpResponse::NotFound().body("Hobby not found"),
        Err(e) => HttpResponse::InternalServerError().body(e.to_string()),
    }
}

Both handlers use imports of actix_web to handle the REST API requests and responses. The MongoDB ObjectID and database handle the ID params and populate the database. The serde_json to handle JSON requests and responses. 

The asynchronous function of create_person and create_hobby has properties DB and person object that get from the CreatePerson DTO/model. MongoDB inserts operations used in that function and then returns the success response or internal server error if something happens.

The asynchronous function of get_persons and get_hobbies has properties DB only. They have MongoDB find operations with empty conditions or filters. Return the results as an array of the ListPerson object. The response returns data as a JSON array or an Internal Server error occurs when something wrong happens.

The asynchronous function of get_person_by_id and get_hobby_by_id has properties DB and ID String. The ID will converted to the ObjectID, so, it can be used for the filter of find_one operation. The response will return data as a single JSON object or Internal Server error occurs when something wrong happens.

The asynchronous function of update_person_by_id, delete_person_by_id, delete_hobby_by_id, and update_hobby_by_id has the same properties as the get by id function except the MongoDB operation they use update_one and delete_one.

Next, open the "mod.rs" and then declare these lines.

pub mod persons;
pub mod hobbies;


Step 6. Create Rust Router

To make the handlers usable as REST API, create a Rust file in the src folder.

touch src/routes.rs

Open that file then add these lines of Rust codes.

use actix_web::web;

use crate::handlers::{
    hobbies::{ create_hobby, delete_hobby_by_id, get_hobbies, get_hobby_by_id, update_hobby_by_id },
    persons::{
        create_person,
        delete_person_by_id,
        get_person_by_id,
        get_persons,
        update_person_by_id,
    },
};

pub fn configure_routes(cfg: &mut web::ServiceConfig) {
    cfg.service(
        web
            ::scope("/api/v1")
            .route("/persons", web::post().to(create_person))
            .route("/persons", web::get().to(get_persons))
            .route("/persons/{id}", web::get().to(get_person_by_id))
            .route("/persons/{id}", web::put().to(update_person_by_id))
            .route("/persons/{id}", web::delete().to(delete_person_by_id))
            .route("/hobbies", web::post().to(create_hobby))
            .route("/hobbies", web::get().to(get_hobbies))
            .route("/hobbies/{id}", web::get().to(get_hobby_by_id))
            .route("/hobbies/{id}", web::put().to(update_hobby_by_id))
            .route("/hobbies/{id}", web::delete().to(delete_hobby_by_id))
    );
}

This route file imports the actix_web to create the routes and all of the created functions from the handlers. The function configure_routes registers all routes with their POST, GET, PUT, and DELETE operations and gives the root path "/api/v1".


Step 7. Wrap Them All Together

To wrap the models, DB connection, and handlers together within the Rust application, open the "main.rs" and then replace all of the existing Rust codes with this.

use actix_web::{ App, HttpServer };
use db::get_mongo_client;

mod db;
mod models;
mod handlers;
mod routes;

use routes::configure_routes;

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    let data = get_mongo_client;

    // Start HTTP server
    HttpServer::new(move || { App::new().app_data(data.clone()).configure(configure_routes) })
        .bind("127.0.0.1:8080")?
        .run().await
}

The asynchronous main function is the entry point for this application in runtime. It registers all of the modules and imports the actix_web as the HttpServer. It also registers the MongoDB connection. In the end, this application will run on localhost with port 8080.


Step 8. Run and Test The Rust MongoDB REST-API

Before running and testing this Rust MongoDB application, ensure the service is running. On Mac type this HomeBrew command.

brew services start [email protected]

Now, run the Rust application by typing this command.

cargo run

Open the new terminal tab then test to POST a person using CURL.

curl -X POST http://127.0.0.1:8080/api/v1/persons \
-H "Content-Type: application/json" \
-d '{"name": "Graydon Hoare", "email": "[email protected]", "phone": "+17887677876"}'

It should return this JSON response.

{"inserted_id":{"$oid":"674bb8610e5ac4fa8127fb6a"}}

To GET a list of persons type this command.

curl http://127.0.0.1:8080/api/v1/persons

It should return this JSON response.

[{"name":"Graydon Hoare","email":"[email protected]"}]

To GET a person by ID type this command.

curl http://127.0.0.1:8080/api/v1/persons/674bb8610e5ac4fa8127fb6a

It should return this JSON response.

{"_id":"674bb8610e5ac4fa8127fb6a","name":"Graydon Hoare","email":"[email protected]","phone":"+17887677876","created_at":"2024-12-01T01:14:09.853109+00:00"}

To UPDATE a person by ID type this command.

curl -X PUT http://127.0.0.1:8080/api/v1/persons/674bb8610e5ac4fa8127fb6a \
-H "Content-Type: application/json" \
-d '{"name": "Graydon Hoare", "email": "[email protected]", "phone": "+17887677876"}'

It should return this JSON response.

Person updated

To DELETE a person by ID type this command.

curl -X DELETE http://127.0.0.1:8080/api/v1/persons/674bb8610e5ac4fa8127fb6a \
-H "Content-Type: application/json"

It should return this JSON response.

Person deleted

Do the same for the Hobby using different URLs "/api/v1/hobbies".

curl -X POST http://127.0.0.1:8080/api/v1/hobbies \
-H "Content-Type: application/json" \
-d '{"hobby_name": "Fishing", "hobby_description": "Fishing a shark in the Ocean", "person": "674bbaf5f9a7eb77c3d66abb"}'

That it's, the Rust and MongoDB Tutorial: Create CRUD REST-API. You can find the full source code from our GitHub.

That is just the basics. If you need more deep learning about Rust lang and frameworks you can take the following cheap course:

Thank!