DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’»

DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’» is a community of 967,611 amazing developers

We're a place where coders share, stay up-to-date and grow their careers.

Create account Log in
Cover image for Build a REST API with Rust and MongoDB - Actix web Version
Demola Malomo for Hackmamba

Posted on • Updated on

Build a REST API with Rust and MongoDB - Actix web Version

REST API has become the De Facto for connecting and transferring data from one source to another. It offers a set of guidelines and architectural patterns for designing and developing web services.

This post will discuss building a user management application with Rust using the Actix web framework and MongoDB. At the end of this tutorial, we will learn how to structure a Rust application, build a REST API and persist our data using MongoDB.

Actix web is an HTTP web framework written in Rust with performance and productivity support. Actix web ships with features like type-safety, reusability, logging, static file serving and much more that developers can leverage to build scalable applications.

MongoDB is a document-based database management program used as an alternative to relational databases. MongoDB supports working with large sets of distributed data with options to store or retrieve information seamlessly.

The complete source code is available in this repository.

Prerequisites

To fully grasp the concepts presented in this tutorial, experience with Rust is required. Experience with MongoDB isn’t a requirement, but it’s nice to have.

We will also be needing the following:

Let’s code

Getting Started

To get started, we need to navigate to the desired directory and run the command below in our terminal

    cargo new actix-mongo-api && cd actix-mongo-api
Enter fullscreen mode Exit fullscreen mode

This command creates a Rust project called actix-mongo-api and navigates into the project directory.

Next, we proceed to install the required dependencies by modifying the [dependencies] section of the Cargo.toml file as shown below:

    //other code section goes here

    [dependencies]
    actix-web = "4"
    serde = "1.0.136"
    dotenv = "0.15.0"
    futures = "0.3"

    [dependencies.mongodb]
    version = "2.2.0"
    default-features = false
    features = ["async-std-runtime"] 
Enter fullscreen mode Exit fullscreen mode

actix-web = "4" is a Rust-based framework for building web applications.

serde = "1.0.136" is a framework for serializing and deserializing Rust data structures. E.g. convert Rust structs to JSON.

dotenv = "0.15.0" is a library for managing environment variables.

futures = "0.3" is a library for doing asynchronous programming in rust

[dependencies.mongodb] is a driver for connecting to MongoDB. It also specifies the required version and the feature type(Asynchronous API).

We need to run the command below to install the dependencies:

    cargo build
Enter fullscreen mode Exit fullscreen mode

Application Entry Point

With the project dependencies installed, modify the main.rs file in the src folder to the following:

    use actix_web::{get, App, HttpResponse, HttpServer, Responder};

    #[get("/")]
    async fn hello() -> impl Responder {
        HttpResponse::Ok().json("Hello from rust and mongoDB")
    }

    #[actix_web::main]
    async fn main() -> std::io::Result<()> {
        HttpServer::new(|| App::new().service(hello))
            .bind(("localhost", 8080))?
            .run()
            .await
    }
Enter fullscreen mode Exit fullscreen mode

The snippet above does the following:

  • Imports the required dependencies
  • Creates a hello handler that uses the Rust macro to specify the HTTP method, the route path /, and returns a JSON of Hello from rust and mongoDB.
  • Uses the #[actix_web::main] macro to run the main function asynchronously within the actix runtime. The main function does the following:
    • Creates a new server using HttpServer struct that uses a closure to serve incoming requests using the App instance. The App also registers the hello handler. HttpServer is the backbone of our application; it takes care of request handling, the maximum number of connections allowed, layered security, e.t.c, while App handles application logic like request handlers, middlewares, routing, e.t.c.
    • Configures the server to run asynchronously and process HTTP requests on localhost:8080.

Next, we can test our application by running the command below in our terminal.

    cargo run
Enter fullscreen mode Exit fullscreen mode

Testing the app

Module system in Rust

A module in Rust is a mechanism for splitting code into reusable components and managing visibility between them. Modules help us maintain a good project structure for our project.

To do this, we need to navigate to the src folder and create api, models, and repository folder with the corresponding mod.rs file to manage visibility.

Updated project folder structure

api is for modularizing API handlers.

models is for modularizing data logics.

repository is for modularizing database logics.

Adding reference to the Modules
To use the code in the modules, we need to declare them as a module and import them into the main.rs file.

    //add the modules
    mod api; 
    mod models;
    mod repository;

    use actix_web::{get, App, HttpResponse, HttpServer, Responder};

    // the remaining part of our code goes here
Enter fullscreen mode Exit fullscreen mode

Setting up MongoDB

With that done, we need to log in or sign up into our MongoDB account. Click the project dropdown menu and click on the New Project button.

New Project

Enter the rust-api as the project name, click Next, and click Create Project..

enter project name
Create Project

Click on Build a Database

Select Shared as the type of database.

Shared highlighted in red

Click on Create to setup a cluster. This might take sometime to setup.

Creating a cluster

Next, we need to create a user to access the database externally by inputting the Username, Password and then clicking on Create User. We also need to add our IP address to safely connect to the database by clicking on the Add My Current IP Address button. Then click on Finish and Close to save changes.

Create user
Add IP

On saving the changes, we should see a Database Deployments screen, as shown below:

Database Screen

Connecting our application to MongoDB

With the configuration done, we need to connect our application with the database created. To do this, click on the Connect button

Connect to database

Click on Connect your application, change the Driver to Rust and the Version as shown below. Then click on the copy icon to copy the connection string.

connect application
Copy connection string

Setup Environment Variable
Next, we must modify the copied connection string with the user's password we created earlier and change the database name. To do this, first, we need to create a .env file in the root directory, and in this file, add the snippet copied:

    MONGOURI=mongodb+srv://<YOUR USERNAME HERE>:<YOUR PASSWORD HERE>@cluster0.e5akf.mongodb.net/myFirstDatabese?retryWrites=true&w=majority
Enter fullscreen mode Exit fullscreen mode

Sample of a properly filled connection string below:

MONGOURI=mongodb+srv://malomz:malomzPassword@cluster0.e5akf.mongodb.net/golangDB?retryWrites=true&w=majority
Enter fullscreen mode Exit fullscreen mode

Creating REST APIs

With the setup done, we need to create a model to represent our application data. To do this, we need to navigate to the models folder, and in this folder, create a user_model.rs file and add the snippet below:

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

    #[derive(Debug, Serialize, Deserialize)]
    pub struct User {
        #[serde(rename = "_id", skip_serializing_if = "Option::is_none")]
        pub id: Option<ObjectId>,
        pub name: String,
        pub location: String,
        pub title: String,
    }
Enter fullscreen mode Exit fullscreen mode

The snippet above does the following:

  • Imports the required dependencies
  • Uses the derive macro to generate implementation support for formatting the output, serializing, and deserializing the data structure.
  • Creates a User struct with required properties. We also added field attributes to the id property to rename and ignore the field if it is empty.

PS: The pub modifier makes the struct and its property public and can be accessed from other files/modules.

Next, we must register the user_model.rs file as part of the models module. To do this, open the mod.rs in the models folder and add the snippet below:

    pub mod user_model;
Enter fullscreen mode Exit fullscreen mode

Create a User Endpoint
With the model fully set up and made available to be consumed, we can now create our database logic to create a user. To do this, First, we need to navigate to the repository folder, and in this folder, create a mongodb_repo.rs file and add the snippet below:

    use std::env;
    extern crate dotenv;
    use dotenv::dotenv;

    use mongodb::{
        bson::{extjson::de::Error},
        results::{ InsertOneResult},
        Client, Collection,
    };
    use crate::models::user_model::User;

    pub struct MongoRepo {
        col: Collection<User>,
    }

    impl MongoRepo {
        pub async fn init() -> Self {
            dotenv().ok();
            let uri = match env::var("MONGOURI") {
                Ok(v) => v.to_string(),
                Err(_) => format!("Error loading env variable"),
            };
            let client = Client::with_uri_str(uri).unwrap();
            let db = client.database("rustDB");
            let col: Collection<User> = db.collection("User");
            MongoRepo { col }
        }

        pub async fn create_user(&self, new_user: User) -> Result<InsertOneResult, Error> {
            let new_doc = User {
                id: None,
                name: new_user.name,
                location: new_user.location,
                title: new_user.title,
            };
            let user = self
                .col
                .insert_one(new_doc, None)
                .await
                .ok()
                .expect("Error creating user");
            Ok(user)
        }
    }
Enter fullscreen mode Exit fullscreen mode

The snippet above does the following:

  • Imports the required dependencies
  • Creates a MongoRepo struct with a col field to access MongoDB collection
  • Creates an implementation block that adds methods to the MongoRepo struct
  • Adds an init method to the implementation block to load the environment variable, creates a connection to the database, and returns an instance of the MongoRepo struct
  • Adds a create_user method that takes in a self and new_user as parameters and returns the created user or an error. Inside the method, we created a new document using the User struct. Then we use the self referencing the MongoRepo struct to access the insert_one function from the collection to create a new user and handle errors. Finally, we returned the created user information.

PS: The None specified when creating a new document tells MongoDB to automatically generate the user’s id.

Next, we must register the mongodb_repo.rs file as part of the repository module. To do this, open the mod.rs in the repository folder and add the snippet below:

    pub mod mongodb_repos;
Enter fullscreen mode Exit fullscreen mode

Secondly, we need to create a handler that uses the create_user method from the repository to create a user. To do this, we need to navigate to the api folder, and in this folder, create a user_api.rs file and add the snippet below:

    use crate::{models::user_model::User, repository::mongodb_repo::MongoRepo};
    use actix_web::{
        post,
        web::{Data, Json},
        HttpResponse,
    };

    #[post("/user")]
    pub async fn create_user(db: Data<MongoRepo>, new_user: Json<User>) -> HttpResponse {
        let data = User {
            id: None,
            name: new_user.name.to_owned(),
            location: new_user.location.to_owned(),
            title: new_user.title.to_owned(),
        };
        let user_detail = db.create_user(data).await;
        match user_detail {
            Ok(user) => HttpResponse::Ok().json(user),
            Err(err) => HttpResponse::InternalServerError().body(err.to_string()),
        }
    }
Enter fullscreen mode Exit fullscreen mode

The snippet above does the following:

  • Imports the required dependencies
  • Uses the routing macro to specify HTTP method and corresponding route
  • Creates a create_user handler that takes in the db, a type to the MongoRepo and a new_user as parameters. Inside the handler, we created a data variable for creating a user, inserted it into the database using the db.create_user method, and returned the correct response if the insert was successful or error if any.

PS: The Data and Json struct used in defining the parameter is for managing application state shared across routes and extracting JSON data from request payloads, respectively.

Finally, we need to modify our application entry point to include the create_user handler. To do this, we need to navigate to the main.rs file and modify it as shown below:

    mod api;
    mod models;
    mod repository;

    //modify imports below
    use actix_web::{web::Data, App, HttpServer};
    use api::user_api::{create_user};
    use repository::mongodb_repo::MongoRepo;

    #[actix_web::main]
    async fn main() -> std::io::Result<()> {
        let db = MongoRepo::init().await;
        let db_data = Data::new(db);
        HttpServer::new(move || {
            App::new()
                .app_data(db_data.clone())
                .service(create_user)
        })
        .bind(("127.0.0.1", 8080))?
        .run()
        .await
    }
Enter fullscreen mode Exit fullscreen mode

The snippet above does the following:

  • Imports the required dependencies
  • Creates a db variable to establish a connection to MongoDB by calling the init() method and adds it to a new instance of the Data struct so that the database state can be available across the application scope.
  • Uses the app_data and service function to add the application data and the handler to the App instance.

PS: The move keyword attached to the closure gives it ownership of the MongoDB configuration.

Get a User Endpoint
To get the details of a user, we must first modify the mongodb_repo.rs file by adding a get_user method to the implementation block.

    use std::env;
    extern crate dotenv;
    use dotenv::dotenv;

    use mongodb::{
        bson::{extjson::de::Error, oid::ObjectId, doc}, //modify here
        results::{ InsertOneResult},
        Client, Collection,
    };
    use crate::models::user_model::User;

    pub struct MongoRepo {
        col: Collection<User>,
    }

    impl MongoRepo {
        pub async fn init() -> Self {
            //init code goes here
        }

        pub async fn create_user(&self, new_user: User) -> Result<InsertOneResult, Error> {
            //create_user code goes here
        }

        pub async fn get_user(&self, id: &String) -> Result<User, Error> {
            let obj_id = ObjectId::parse_str(id).unwrap();
            let filter = doc! {"_id": obj_id};
            let user_detail = self
                .col
                .find_one(filter, None)
                .await
                .ok()
                .expect("Error getting user's detail");
            Ok(user_detail.unwrap())
        }
    }
Enter fullscreen mode Exit fullscreen mode

The snippet above does the following:

  • Modifies the dependencies to include oid::ObjectId and doc
  • Adds a get_user method that takes in a self and id as parameters and returns the user detail or an error. Inside the method, we converted the id to an ObjectId and used it as a filter to get matching document. Then we use the self referencing the MongoRepo struct to access the find_one function from the collection to get the details of the user and handle errors. Finally, we returned the created user information.

Secondly, we need to modify user_api.rs by creating a handler that uses the get_user method from the repository to get a user.

    use crate::{models::user_model::User, repository::mongodb_repo::MongoRepo};
    use actix_web::{
        post, get, //modify here
        web::{Data, Json, Path}, //modify here
        HttpResponse,
    };

    #[post("/user")]
    pub async fn create_user(db: Data<MongoRepo>, new_user: Json<User>) -> HttpResponse {
        //create_user code goes here
    }

    #[get("/user/{id}")]
    pub async fn get_user(db: Data<MongoRepo>, path: Path<String>) -> HttpResponse {
        let id = path.into_inner();
        if id.is_empty() {
            return HttpResponse::BadRequest().body("invalid ID");
        }
        let user_detail = db.get_user(&id).await;
        match user_detail {
            Ok(user) => HttpResponse::Ok().json(user),
            Err(err) => HttpResponse::InternalServerError().body(err.to_string()),
        }
    }
Enter fullscreen mode Exit fullscreen mode

The snippet above does the following:

  • Modifies the dependencies to include get and Path
  • Uses the routing macro to specify HTTP method, corresponding route and route parameter
  • Creates a get_user handler that takes in the db, a type to the MongoRepo and a path for accessing route path as parameters. Inside the handler, we created an id variable to get the user’s id, get the user’s details from the database using the db.get_user method. We returned the correct response if the request was successful or error if any.

Finally, we need to modify our application entry point(main.rs)to include the get_user handler by importing the handler and adding a new service for it.

    mod api;
    mod models;
    mod repository;

    //modify imports below
    use actix_web::{web::Data, App, HttpServer};
    use api::user_api::{create_user, get_user}; //import the handler here
    use repository::mongodb_repo::MongoRepo;

    #[actix_web::main]
    async fn main() -> std::io::Result<()> {
        let db = MongoRepo::init().await;
        let db_data = Data::new(db);
        HttpServer::new(move || {
            App::new()
                .app_data(db_data.clone())
                .service(create_user)
                .service(get_user) //add this
        })
        .bind(("127.0.0.1", 8080))?
        .run()
        .await
    }
Enter fullscreen mode Exit fullscreen mode

Edit a User Endpoint
To edit a user, we must first modify the mongodb_repo.rs file by adding an edit_user method to the implementation block.

    use std::env;
    extern crate dotenv;
    use dotenv::dotenv;

    use mongodb::{
        bson::{extjson::de::Error, oid::ObjectId, doc}, 
        results::{ InsertOneResult, UpdateResult}, //modify here
        Client, Collection,
    };
    use crate::models::user_model::User;

    pub struct MongoRepo {
        col: Collection<User>,
    }

    impl MongoRepo {
        pub async fn init() -> Self {
            //init code goes here
        }

        pub async fn create_user(&self, new_user: User) -> Result<InsertOneResult, Error> {
            //create_user code goes here
        }

        pub async fn get_user(&self, id: &String) -> Result<User, Error> {
            //get_user code goes here
        }

        pub async fn update_user(&self, id: &String, new_user: User) -> Result<UpdateResult, Error> {
            let obj_id = ObjectId::parse_str(id).unwrap();
            let filter = doc! {"_id": obj_id};
            let new_doc = doc! {
                "$set":
                    {
                        "id": new_user.id,
                        "name": new_user.name,
                        "location": new_user.location,
                        "title": new_user.title
                    },
            };
            let updated_doc = self
                .col
                .update_one(filter, new_doc, None)
                .await
                .ok()
                .expect("Error updating user");
            Ok(updated_doc)
        }
    }
Enter fullscreen mode Exit fullscreen mode

The snippet above does the following:

  • Modifies the dependencies to include UpdateResult
  • Adds an update_user method that takes in a self, id, and new_user parameters and returns the updated user detail or an error. Inside the method, we converted the id to an ObjectId, created a filter variable to get the matching document we wanted to update and used the doc macro to update the document fields. Then we use the self referencing the MongoRepo struct to access the update_one function from the collection to update the user matching the filter specified and handle errors. Finally, we returned the updated user information.

Secondly, we need to modify user_api.rs by creating a handler that uses the update_user method from the repository to update a user.

    use crate::{models::user_model::User, repository::mongodb_repo::MongoRepo};
    use actix_web::{
        post, get, put, //modify here
        web::{Data, Json, Path},
        HttpResponse,
    };
    use mongodb::bson::oid::ObjectId; //add this

    #[post("/user")]
    pub async fn create_user(db: Data<MongoRepo>, new_user: Json<User>) -> HttpResponse {
        //create_user code goes here
    }

    #[get("/user/{id}")]
    pub async fn get_user(db: Data<MongoRepo>, path: Path<String>) -> HttpResponse {
        //get_user code goes here
    }

    #[put("/user/{id}")]
    pub async fn update_user(
        db: Data<MongoRepo>,
        path: Path<String>,
        new_user: Json<User>,
    ) -> HttpResponse {
        let id = path.into_inner();
        if id.is_empty() {
            return HttpResponse::BadRequest().body("invalid ID");
        };
        let data = User {
            id: Some(ObjectId::parse_str(&id).unwrap()),
            name: new_user.name.to_owned(),
            location: new_user.location.to_owned(),
            title: new_user.title.to_owned(),
        };
        let update_result = db.update_user(&id, data).await;
        match update_result {
            Ok(update) => {
                if update.matched_count == 1 {
                    let updated_user_info = db.get_user(&id).await;
                    return match updated_user_info {
                        Ok(user) => HttpResponse::Ok().json(user),
                        Err(err) => HttpResponse::InternalServerError().body(err.to_string()),
                    };
                } else {
                    return HttpResponse::NotFound().body("No user found with specified ID");
                }
            }
            Err(err) => HttpResponse::InternalServerError().body(err.to_string()),
        }
    }
Enter fullscreen mode Exit fullscreen mode

The snippet above does the following:

  • Modifies the dependencies to include put and ObjectId
  • Uses the routing macro to specify HTTP method, corresponding route and route parameter
  • Creates an update_user handler that takes in the db, a type to the MongoRepo, path, and new_user as parameters. Inside the handler, we created an id variable to get the user’s id, update the user’s details from the database using the db.update_user method by passing in the updated user’s information. Finally, we checked if the update was successful and returned the updated user or error if any.

Finally, we need to modify our application entry point(main.rs)to include the update_user handler by importing the handler and adding a new service for it.

    mod api;
    mod models;
    mod repository;

    //modify imports below
    use actix_web::{web::Data, App, HttpServer};
    use api::user_api::{create_user, get_user, update_user}; //import the handler here
    use repository::mongodb_repo::MongoRepo;

    #[actix_web::main]
    async fn main() -> std::io::Result<()> {
        let db = MongoRepo::init().await;
        let db_data = Data::new(db);
        HttpServer::new(move || {
            App::new()
                .app_data(db_data.clone())
                .service(create_user)
                .service(get_user)
                .service(update_user) //add this
        })
        .bind(("127.0.0.1", 8080))?
        .run()
        .await
    }

Enter fullscreen mode Exit fullscreen mode

Delete a User Endpoint
To delete a user, we must first modify the mongodb_repo.rs file by adding an delete_user method to the implementation block.

    use std::env;
    extern crate dotenv;
    use dotenv::dotenv;

    use mongodb::{
        bson::{extjson::de::Error, oid::ObjectId, doc}, 
        results::{ InsertOneResult, UpdateResult, DeleteResult}, //modify here
        Client, Collection,
    };
    use crate::models::user_model::User;

    pub struct MongoRepo {
        col: Collection<User>,
    }

    impl MongoRepo {
        pub async fn init() -> Self {
            //init code goes here
        }

        pub async fn create_user(&self, new_user: User) -> Result<InsertOneResult, Error> {
            //create_user code goes here
        }

        pub async fn get_user(&self, id: &String) -> Result<User, Error> {
            //get_user code goes here
        }

        pub async fn update_user(&self, id: &String, new_user: User) -> Result<UpdateResult, Error> {
            //update_user code goes here
        }

        pub async fn delete_user(&self, id: &String) -> Result<DeleteResult, Error> {
            let obj_id = ObjectId::parse_str(id).unwrap();
            let filter = doc! {"_id": obj_id};
            let user_detail = self
                .col
                .delete_one(filter, None)
                .await
                .ok()
                .expect("Error deleting user");
            Ok(user_detail)
        }
    }
Enter fullscreen mode Exit fullscreen mode

The snippet above does the following:

  • Modifies the dependencies to include DeleteResult
  • Adds a delete_user method that takes in a self and id as parameters and returns the deleted user detail or an error. Inside the method, we converted the id to an ObjectId and created a filter variable to get the matching document we wanted to delete. Then we use the self referencing the MongoRepo struct to access the delete_one function from the collection to delete the user matching the filter specified and handle errors. Finally, we returned the deleted user information.

Secondly, we need to modify user_api.rs by creating a handler that uses the delete_user method from the repository to delete a user.

    use crate::{models::user_model::User, repository::mongodb_repo::MongoRepo};
    use actix_web::{
        post, get, put, delete, //modify here
        web::{Data, Json, Path},
        HttpResponse,
    };
    use mongodb::bson::oid::ObjectId; //add this

    #[post("/user")]
    pub async fn create_user(db: Data<MongoRepo>, new_user: Json<User>) -> HttpResponse {
        //create_user code goes here
    }

    #[get("/user/{id}")]
    pub async fn get_user(db: Data<MongoRepo>, path: Path<String>) -> HttpResponse {
        //get_user code goes here
    }

    #[put("/user/{id}")]
    pub async fn update_user(
        db: Data<MongoRepo>,
        path: Path<String>,
        new_user: Json<User>,
    ) -> HttpResponse {
        //update_user code goes here
    }

    #[delete("/user/{id}")]
    pub async fn delete_user(db: Data<MongoRepo>, path: Path<String>) -> HttpResponse {
        let id = path.into_inner();
        if id.is_empty() {
            return HttpResponse::BadRequest().body("invalid ID");
        };
        let result = db.delete_user(&id).await;
        match result {
            Ok(res) => {
                if res.deleted_count == 1 {
                    return HttpResponse::Ok().json("User successfully deleted!");
                } else {
                    return HttpResponse::NotFound().json("User with specified ID not found!");
                }
            }
            Err(err) => HttpResponse::InternalServerError().body(err.to_string()),
        }
    }
Enter fullscreen mode Exit fullscreen mode

The snippet above does the following:

  • Modifies the dependencies to include delete
  • Uses the routing macro to specify HTTP method, corresponding route and route parameter
  • Creates a delete_user handler that takes in the db, a type to the MongoRepo and path as parameters. Inside the handler, we created an id variable to get the user’s id and ****delete the user from the database using the db.delete_user method by passing in the id. Finally, we returned the appropriate response or error if any.

Finally, we need to modify our application entry point(main.rs)to include the delete_user handler by importing the handler and adding a new service for it.

    mod api;
    mod models;
    mod repository;

    //modify imports below
    use actix_web::{web::Data, App, HttpServer};
    use api::user_api::{create_user, get_user, update_user, delete_user}; //import the handler here
    use repository::mongodb_repo::MongoRepo;

    #[actix_web::main]
    async fn main() -> std::io::Result<()> {
        let db = MongoRepo::init().await;
        let db_data = Data::new(db);
        HttpServer::new(move || {
            App::new()
                .app_data(db_data.clone())
                .service(create_user)
                .service(get_user)
                .service(update_user) 
                .service(delete_user) //add this
        })
        .bind(("127.0.0.1", 8080))?
        .run()
        .await
    }
Enter fullscreen mode Exit fullscreen mode

Get all Users Endpoint
To get the list of users, we must first modify the mongodb_repo.rs file by adding a get_all_users method to the implementation block.

    use std::env;
    extern crate dotenv;
    use dotenv::dotenv;

    use mongodb::{
        bson::{extjson::de::Error, oid::ObjectId, doc}, 
        results::{ InsertOneResult, UpdateResult, DeleteResult},
        Client, Collection,
    };
    use futures::stream::TryStreamExt; //add this
    use crate::models::user_model::User;

    pub struct MongoRepo {
        col: Collection<User>,
    }

    impl MongoRepo {
        pub async fn init() -> Self {
            //init code goes here
        }

        pub async fn create_user(&self, new_user: User) -> Result<InsertOneResult, Error> {
            //create_user code goes here
        }

        pub async fn get_user(&self, id: &String) -> Result<User, Error> {
            //get_user code goes here
        }

        pub async fn update_user(&self, id: &String, new_user: User) -> Result<UpdateResult, Error> {
            //update_user code goes here
        }

        pub async fn delete_user(&self, id: &String) -> Result<DeleteResult, Error> {
            //delete_user code goes here
        }

        pub async fn get_all_users(&self) -> Result<Vec<User>, Error> {
        let mut cursors = self
            .col
            .find(None, None)
            .await
            .ok()
            .expect("Error getting list of users");
        let mut users: Vec<User> = Vec::new();
        while let Some(user) = cursors
            .try_next()
            .await
            .ok()
            .expect("Error mapping through cursor")
        {
            users.push(user)
        }
        Ok(users)
        }
    }
Enter fullscreen mode Exit fullscreen mode

The snippet above adds a get_all_users method that takes in a self as a parameter and returns the list of users or an error. Inside the method, we use the self referencing the MongoRepo struct to access the find function from the collection without any filter so that it can match all the documents inside the database, returned the list optimally using the try_next() method to loop through the list of users, and handle errors.

Secondly, we need to modify user_api.rs by creating a handler that uses the get_all_users method from the repository to get list of users.

    use crate::{models::user_model::User, repository::mongodb_repo::MongoRepo};
    use actix_web::{
        post, get, put, delete, 
        web::{Data, Json, Path},
        HttpResponse,
    };
    use mongodb::bson::oid::ObjectId; 

    #[post("/user")]
    pub async fn create_user(db: Data<MongoRepo>, new_user: Json<User>) -> HttpResponse {
        //create_user code goes here
    }

    #[get("/user/{id}")]
    pub async fn get_user(db: Data<MongoRepo>, path: Path<String>) -> HttpResponse {
        //get_user code goes here
    }

    #[put("/user/{id}")]
    pub async fn update_user(
        db: Data<MongoRepo>,
        path: Path<String>,
        new_user: Json<User>,
    ) -> HttpResponse {
        //update_user code goes here
    }

    #[delete("/user/{id}")]
    pub async fn delete_user(db: Data<MongoRepo>, path: Path<String>) -> HttpResponse {
        //delet_user code goes here
    }

    #[get("/users")]
    pub async fn get_all_users(db: Data<MongoRepo>) -> HttpResponse {
        let users = db.get_all_users().await;
        match users {
            Ok(users) => HttpResponse::Ok().json(users),
            Err(err) => HttpResponse::InternalServerError().body(err.to_string()),
        }
    }
Enter fullscreen mode Exit fullscreen mode

The snippet above does the following:

  • Uses the routing macro to specify HTTP method and corresponding route
  • Creates a get_all_users handler that uses the db.delete_user method to get the list of users. Then, we returned the list of users or error if any.

Finally, we need to modify our application entry point(main.rs)to include the get_all_users handler by importing the handler and adding a new service for it.

    mod api;
    mod models;
    mod repository;

    //modify imports below
    use actix_web::{web::Data, App, HttpServer};
    use api::user_api::{create_user, get_user, update_user, delete_user, get_all_users}; //import the handler here
    use repository::mongodb_repo::MongoRepo;

    #[actix_web::main]
    async fn main() -> std::io::Result<()> {
        let db = MongoRepo::init().await;
        let db_data = Data::new(db);
        HttpServer::new(move || {
            App::new()
                .app_data(db_data.clone())
                .service(create_user)
                .service(get_user)
                .service(update_user) 
                .service(delete_user) 
               .service(get_all_users)//add this
        })
        .bind(("127.0.0.1", 8080))?
        .run()
        .await
    }

Enter fullscreen mode Exit fullscreen mode

With that done, we can test our application by running the command below in our terminal.

    cargo run
Enter fullscreen mode Exit fullscreen mode

Create a user endpoint

Get a user endpoint

Edit a user endpoint

Delete a user endpoint

Get list of users endpoint

Database with users document

Conclusion

This post discussed how to modularize a Rust application, build a REST API, and persist our data using MongoDB.

You may find these resources helpful:

Top comments (9)

Collapse
 
kmahar profile image
kaitlin

Hi @malomz, I'm one of the maintainers of the mongodb crate. Thanks for writing up this tutorial!

I wanted to point out that since you are writing an asynchronous application (as Actix is an async framework), it would be most appropriate to use the mongodb async API here rather than the sync API. With the sync API, every time you interact with the database you will be running blocking code on the current thread and preventing the async runtime (Tokio) from scheduling any other async tasks on the thread while waiting for a response from the database. The end result is that your application is more resource-intensive and less performant, since threads cannot be shared as efficiently.
With the async API, the thread can be "given up" to another task while you await the response from the database.
This is a nice blog post on this subject.

The async API is identical to the sync one besides everything being async; to adopt your example here I think you'd just need to mark the methods on your MongoRepo type async and call the equivalent async mongodb API methods.

You can find a simple example of using the async mongodb API with actix here.

Feel free to get in touch with us via GitHub if you have any questions or run into any issues.

Collapse
 
malomz profile image
Demola Malomo Author

Hi @kmahar ,

Thanks for pointing this out πŸ™πŸ™.

I have updated accordinglyπŸ‘

Collapse
 
kmahar profile image
kaitlin

Awesome! One more thing to note is that since Actix uses the tokio runtime, I'd suggest using the driver with tokio as well, rather than async-std, so that you do not need to have two separate runtimes going that cannot coordinate with one another.

By default the mongodb crate uses tokio, so to do this, you can just remove these lines from your Cargo.toml:

default-features = false
features = ["async-std-runtime"] 
Enter fullscreen mode Exit fullscreen mode

There's some documentation on all of the drivers' feature flags available here.

Collapse
 
codewander profile image
codewander

I expect to see more people start to use rust + postgres + an ORM, when they want something with a richer type system or more functional idioms than go, python, or node/ts. Do you think that emerge and possibly reach fourth place over ruby for web dev?

Collapse
 
malomz profile image
Demola Malomo Author

Well, rust is relatively new to the ecosystem as compared to others. Hopefully, it becomes mainstream! :)

Collapse
 
aaravrrrrrr profile image
Aarav Reddy

Thanks!

Collapse
 
snelson1 profile image
Sophia Nelson

Good article, thank you.

Collapse
 
triyanox profile image
Mohamed Achaq

Thanks for putting the effort to write this amazing post definitely will try Actix Web soon 😁

Collapse
 
malomz profile image
Demola Malomo Author

Glad you enjoyed it.
Yea, you should give it a try!

🌚 Friends don't let friends browse without dark mode.

Sorry, it's true.