Getting started with gRPC in Rust
Tags: rust, mongodb, grpc, backend
Published: 2023-05-23 12:48:14

Microservices architecture is one of the preferred methods of building scalable and robust applications. It involves breaking large applications into smaller components that are well-defined, performs a specific task and uses sets of application programming interface (API) for their communication.

Communication is an essential part of microservices; it plays an important role in letting services talk to each other within the larger application context. Some examples of protocol microservices use to communicate with each other includes HTTP, **gRPC**, message brokers, etc.

In this article, we will explore what gRPC is and how to get started by building a user management service using gRPC, MongoDB and Rust.

## What is gRPC?

gRPC is a modern communication framework that can run in any environment and helps connect services efficiently. It was introduced in 2015 and governed by the Cloud Native Computing Platform (CNCF). Beyond efficiently connecting services across a distributed system, mobile applications, frontend to backend, etc., it supports health checking, load balancing, tracing, and authentication.

gRPC offers a fresh perspective to developers building medium to complex applications as it can generate client and server bindings for multiple languages. The following are some of its benefits:

**Service definition**

gRPC uses Protocol Buffers as its interface description language, similar to JSON and provides features like authentication, cancellation, timeouts, etc.

**Lightweight and performant**

gRPC definitions are 30 percent smaller than JSON definitions and are 5 to 7 times faster than a traditional REST API.

**Multiple platform support**

gRPC is language agnostic and has automated code generation for client and server-supported languages.

**Scalable**

From the developer’s environment to production, gRPC is designed to scale millions ler seconds requests.

# Getting started

Now that we understand gRPC's importance in building scalable applications, let’s build a user management service with gRPC, MongoDB, and Rust. The project source code can be found [here](https://github.com/Mr-Malomz/grpc_rust).

## Prerequisites

To fully grasp the concepts presented in this tutorial, the following are required:

-   Basic understanding of Rust
-   Basic understanding of Protocol Buffer
-   Protocol Buffer [compiler](https://grpc.io/docs/protoc-installation/?utm_source=fullstackwriter&utm_medium=fullstackwriter-blog) installed
-   A [MongoDB account](https://www.mongodb.com/?utm_source=fullstackwriter&utm_medium=fullstackwriter-blog) to host the database. [**Signup**](https://www.mongodb.com/cloud/atlas/register?utm_source=fullstackwriter&utm_medium=fullstackwriter-blog) **is completely free**.
-   [Postman](https://www.postman.com/downloads/?utm_source=fullstackwriter&utm_medium=fullstackwriter-blog) or any gRPC testing application

## Project and Dependencies setup

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

```bash
cargo new grpc_rust && cd grpc_rust
```

This command creates a Rust project called `grpc_rust` and navigates into the project directory.

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

```toml
//other code section goes here

[dependencies]
tokio = {version = "1", features = ["macros", "rt-multi-thread"]}
serde = {versiom = "1", features = ["derive"]}
dotenv = "0.15.0"
tonic = "0.9.2"
prost = "0.11.9"
futures = "0.3"

[dependencies.mongodb]
version = "2.2.0"

[build-dependencies]
tonic-build = "0.9.2"
```

`tokio = {version = "1", features = ["macros", "rt-multi-thread"]}` is a runtime that enables asynchronous programming in Rust.

`serde = {versiom = "1", features = ["derive"]}` is a framework for serializing and deserializing Rust data structures.

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

`tonic = "0.9.2"` is a Rust implementation of gRPC.

`prost = "0.11.9"` is a Protocol Buffers implementation in Rust and generates simple, idiomatic Rust code from `proto2` and `proto3` files.

`futures = "0.3"` is a library for doing asynchronous programming with MongoDB driver

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

`[build-dependencies]` specifies `tonic-build = "0.9.2"` as a dependency. It compiles `.proto` files into Rust code.

## Defining the user management Protocol Buffer and compilation

To get started, we need to define a Protocol Buffer to represent all the operations and responses involved in the user management service. To do this, first, we need to create a `proto` folder in the root directory, and in this folder, create a `user.proto` file and add the snippet below:

```proto
syntax = "proto3";
package user;

service UserService {
    rpc GetUser (UserRequest) returns (UserResponse);
    rpc CreateUser (CreateUserRequest) returns (CreateUserResponse);
    rpc UpdateUser (UpdateUserRequest) returns (UpdateUserResponse);
    rpc DeleteUser (DeleteUserRequest) returns (DeleteUserResponse);
    rpc GetAllUsers (Empty) returns (GetAllUsersResponse);
}

message UserRequest {
    string id = 1;
}

message UserResponse {
    string id = 1;
    string name = 2;
    string location = 3;
    string title = 4;
}

message CreateUserRequest {
    string name = 2;
    string location = 3;
    string title = 4;
}

message CreateUserResponse {
    string data = 1;
}

message UpdateUserRequest {
    string _id = 1;
    string name = 2;
    string location = 3;
    string title = 4;
}

message UpdateUserResponse {
    string data = 1;
}

message DeleteUserRequest {
    string id = 1;
}

message DeleteUserResponse {
    string data = 1;
}

message Empty {}

message GetAllUsersResponse {
    repeated UserResponse users = 1;
}
```

The snippet above does the following:

-   Specifies the use of `proto3` syntax
-   Declares `user` as the package name
-   Creates a `service` to Create, Read, Edit, and Delete (CRUD) a user and their corresponding responses as `message`s.

Secondly, we need to create a build file that instructs `tonic-build = "0.9.2"` dependency to compile our `user.proto` file into a Rust code. To do this, we need to create a `build.rs` file in the root directory and add the snippet below:

```rust
fn main() -> Result<(), Box<dyn std::error::Error>> {
    tonic_build::compile_protos("proto/user.proto")?;
    Ok(())
}
```

Lastly, we need to compile the `user.proto` file using the `build.rs` instruction we specified earlier by running the command below in our terminal:

```bash
cargo build
```

## Using the generated code from gRPC in our application

With the build process done, we can start using the generated code in our application.

**Database setup and integration**

First, we need to set up a database and a collection on MongoDB as shown below:

![database setup](https://paper-attachments.dropboxusercontent.com/s_80F8DDD905BBAA506022F629DD8AAC7F4C11E43711F085CB17CB09D07EC7D70E_1683330895647_Screenshot+2023-05-06+at+00.54.33.png)

We also need to get our database connection string by clicking on the **Connect** button and changing the Driver to `Rust`.

![](https://paper-attachments.dropboxusercontent.com/s_80F8DDD905BBAA506022F629DD8AAC7F4C11E43711F085CB17CB09D07EC7D70E_1683331705497_Screenshot+2023-05-06+at+00.57.31.png)

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

```bash
MONGOURI=mongodb+srv://<YOUR USERNAME HERE>:<YOUR PASSWORD HERE>@cluster0.e5akf.mongodb.net/<DATABASE NAME>?retryWrites=true&w=majority
```

Sample of a properly filled connection string below:

```bash
MONGOURI=mongodb+srv://malomz:malomzPassword@cluster0.e5akf.mongodb.net/rustDB?retryWrites=true&w=majority
```

Lastly, we need to navigate to the `src` folder, create a `mongo_connection.rs` file to implement our database logic and add the snippet below:

```rust
use std::{env, io::Error};

use dotenv::dotenv;
use futures::TryStreamExt;
use mongodb::bson::doc;
use mongodb::bson::oid::ObjectId;
use mongodb::results::{DeleteResult, InsertOneResult, UpdateResult};
use mongodb::{Client, Collection};
use serde::{Deserialize, Serialize};

#[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,
}

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

impl DBMongo {
    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)
            .await
            .expect("error connecting to database");
        let col = client.database("rustDB").collection("User");
        DBMongo { col }
    }

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

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

    pub async fn update_user(id: String, new_user: User) -> Result<UpdateResult, Error> {
        let db = DBMongo::init().await;
        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 = db
            .col
            .update_one(filter, new_doc, None)
            .await
            .ok()
            .expect("Error updating user");
        Ok(updated_doc)
    }

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

    pub async fn get_all_users() -> Result<Vec<User>, Error> {
        let db = DBMongo::init().await;
        let mut cursors = db
            .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)
    }
}
```

The snippet above does the following:

-   **Line: 1 - 9**: Imports the required dependencies
-   **Line: 11 - 18**: 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.
-   **Line: 20 - 22**: Creates a `DBMongo` struct with a `col` field to access MongoDB collection
-   **Line: 24 - 122**: Creates an implementation block that adds methods to the `MongoRepo` struct to initialize the database with its corresponding CRUD operation.

**Integrating the database logic with gRPC-generated code**

With our database logic setup, we can use the methods to create our application handlers. To do this, we need to create a `service.rs` file inside the same `src` folder and add the snippet below:

```rust
use mongodb::bson::oid::ObjectId;
use tonic::{Request, Response, Status};
use user::{
    user_service_server::UserService, CreateUserRequest, CreateUserResponse, DeleteUserRequest,
    DeleteUserResponse, Empty, GetAllUsersResponse, UpdateUserRequest, UpdateUserResponse,
};
use crate::mongo_connection::{self, DBMongo};
use self::user::{UserRequest, UserResponse};

pub mod user {
    tonic::include_proto!("user");
}

#[derive(Debug, Default)]
pub struct User {}

#[tonic::async_trait]
impl UserService for User {
    async fn create_user(
        &self,
        request: Request<CreateUserRequest>,
    ) -> Result<Response<CreateUserResponse>, Status> {
        let req = request.into_inner();
        let new_user = mongo_connection::User {
            id: None,
            name: req.name,
            location: req.location,
            title: req.title,
        };
        let db = DBMongo::create_user(new_user).await;
        match db {
            Ok(resp) => {
                let user = CreateUserResponse {
                    data: resp.inserted_id.to_string(),
                };
                Ok(Response::new(user))
            }
            Err(error) => Err(Status::aborted(format!("{}", error))),
        }
    }

    async fn get_user(
        &self,
        request: Request<UserRequest>,
    ) -> Result<Response<UserResponse>, Status> {
        let req = request.into_inner();
        let db = DBMongo::get_user(req.id).await;
        match db {
            Ok(resp) => {
                let user = UserResponse {
                    id: resp.id.unwrap().to_string(),
                    name: resp.name,
                    location: resp.location,
                    title: resp.title,
                };
                Ok(Response::new(user))
            }
            Err(error) => Err(Status::aborted(format!("{}", error))),
        }
    }

    async fn update_user(
        &self,
        request: Request<UpdateUserRequest>,
    ) -> Result<Response<UpdateUserResponse>, Status> {
        let req = request.into_inner();
        let new_user = mongo_connection::User {
            id: Some(ObjectId::parse_str(req.id.clone()).unwrap()),
            name: req.name,
            location: req.location,
            title: req.title,
        };
        let db = DBMongo::update_user(req.id.clone(), new_user).await;
        match db {
            Ok(_) => {
                let user = UpdateUserResponse {
                    data: String::from("User details updated successfully!"),
                };
                Ok(Response::new(user))
            }
            Err(error) => Err(Status::aborted(format!("{}", error))),
        }
    }

    async fn delete_user(
        &self,
        request: Request<DeleteUserRequest>,
    ) -> Result<Response<DeleteUserResponse>, Status> {
        let req = request.into_inner();
        let db = DBMongo::delete_user(req.id).await;
        match db {
            Ok(_) => {
                let user = DeleteUserResponse {
                    data: String::from("User details deleted successfully!"),
                };
                Ok(Response::new(user))
            }
            Err(error) => Err(Status::aborted(format!("{}", error))),
        }
    }

    async fn get_all_users(
        &self,
        _: Request<Empty>,
    ) -> Result<Response<GetAllUsersResponse>, Status> {
        let db = DBMongo::get_all_users().await;
        match db {
            Ok(resp) => {
                let mut user_list: Vec<UserResponse> = Vec::new();
                for data in resp {
                    let mapped_user = UserResponse {
                        id: data.id.unwrap().to_string(),
                        name: data.name,
                        location: data.location,
                        title: data.title,
                    };
                    user_list.push(mapped_user);
                }
                let user = GetAllUsersResponse { users: user_list };
                Ok(Response::new(user))
            }
            Err(error) => Err(Status::aborted(format!("{}", error))),
        }
    }
}
```

The snippet above does the following:

-   **Line: 1 - 8**: Imports the required dependencies (including the gRPC-generated)
-   **Line: 10 - 12**: Declares `user` struct to bring into scope our gRPC-generated code using `tonic::include_proto!("user")`
-   **Line: 14 - 15**: Creates a `User` struct to represent our application model
-   **Line: 17 - 125**: Implements the `UserService` traits from the gRPC-generated code for the `User` struct by creating required methods and returning appropriate responses as generated by gRPC

**Creating the server**

With that done, we can create the application gRPC server by modifying the `main.rs` file as shown below:

```rust
use std::net::SocketAddr;

use service::{user::user_service_server::UserServiceServer, User};
use tonic::transport::Server;

mod mongo_connection;
mod service;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let address: SocketAddr = "[::1]:8080".parse().unwrap();
    let user = User::default();

    Server::builder()
        .add_service(UserServiceServer::new(user))
        .serve(address)
        .await?;
    Ok(())
}
```

The snippet above does the following:

-   Imports the required dependencies and adds the `mongo_connection` and `service` as a module
-   Creates a server using `Server::builder()` method and adds the `UserServiceServer` as a service.

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

```bash
cargo run
```

## Testing with Postman

With our server up and running, we can test our application by creating a new **gRPC Request**.

![Create new](https://paper-attachments.dropboxusercontent.com/s_80F8DDD905BBAA506022F629DD8AAC7F4C11E43711F085CB17CB09D07EC7D70E_1683337484831_Screenshot+2023-04-28+at+00.18.35.png)

![Select gRPC Request](https://paper-attachments.dropboxusercontent.com/s_80F8DDD905BBAA506022F629DD8AAC7F4C11E43711F085CB17CB09D07EC7D70E_1683337484805_Screenshot+2023-04-28+at+00.19.06.png)

Input `grpc://[::1]:8080` as the URL, select the **Import a .proto file** option and upload the `user.proto` file we created earlier.

![URL and upload proto file](https://paper-attachments.dropboxusercontent.com/s_80F8DDD905BBAA506022F629DD8AAC7F4C11E43711F085CB17CB09D07EC7D70E_1683337591306_Screenshot+2023-04-28+at+00.27.32.png)

With that done, the corresponding method will be populated and we can test them accordingly.

![getAllUsers](https://paper-attachments.dropboxusercontent.com/s_80F8DDD905BBAA506022F629DD8AAC7F4C11E43711F085CB17CB09D07EC7D70E_1683337760054_Screenshot+2023-04-28+at+00.43.19.png)
![getAUser](https://paper-attachments.dropboxusercontent.com/s_80F8DDD905BBAA506022F629DD8AAC7F4C11E43711F085CB17CB09D07EC7D70E_1683337760024_Screenshot+2023-04-28+at+00.43.59.png)

We can also validate that our gRPC server works by checking our MongoDB collection

![Collection](https://paper-attachments.dropboxusercontent.com/s_80F8DDD905BBAA506022F629DD8AAC7F4C11E43711F085CB17CB09D07EC7D70E_1683337857960_Screenshot+2023-04-28+at+00.45.10.png)

## Conclusion

This post discussed what gRPC is, its role in building scalable applications and how to get started by building a user management service with Rust and MongoDB. Beyond what was discussed above, gRPC offers robust techniques around authentication, error handling, performance, etc.

These resources might be helpful:

-   [Tonic](https://github.com/hyperium/tonic)
-   [Tokio runtime](https://tokio.rs/)
-   [MongoDB Rust driver](https://github.com/mongodb/mongo-rust-driver)
-   [Serde (Serializing and Deserializing library)](https://serde.rs/)