Rust Rest API : actix & mongo db driver

Héla Ben Khalfallah
7 min readJul 5, 2020

--

What we will do ?

In this story, we will see together how to make a Rust Rest API using :

  • actix as http server.
  • mongo db driver to dialog with mongo database.
  • postman to test our endpoints.
Technical staff

Project configuration

Create & init new project

cargo new rust-rest-api
cd rust-rest-api
cargo init

Project structure

Rust project structure

Dependencies (Cargo.toml)

[package]
name = "rust-rest-api"
version = "0.1.0"
authors = ["helabenkhalfallah <helabenkhalfallah@gmail.com>"]
edition = "2018"

[dependencies]
actix-web = "2"
actix-rt = "1"
mongodb = "0.9.2"
bson = "0.14.0"
serde = "1.0.103"
futures = "0.3.4"
env_logger = "0.7"
dotenv = "0.15.0"

Then run :

cargo build

cargo build will download dependencies and build project.

dotenv configuration

1/ Create a .env file :

DATABASE_URL = mongodb://localhost:27017
DATABASE_NAME = local
USER_COLLECTION_NAME = User
SERVER_URL = 0.0.0.0:4000

2/ Init dotenv in main.rs :

use dotenv::dotenv;fn main() -> std::io::Result<()> {
// init env
dotenv().ok();
}

3/ Using dotenv variables :

// server url
let server_url = env::var("SERVER_URL").expect("SERVER_URL is not set in .env file");

// Parse a connection string into an options struct.
let database_url = env::var("DATABASE_URL").expect("DATABASE_URL is not set in .env file");

Project architecture

Project architecture

Configure http server

Transform main function (async)

use actix_web::{middleware, App, HttpServer};#[actix_rt::main]
async fn main() -> std::io::Result<()> {
// init env
dotenv().ok();
}

Add log middleware

actix had a middleware concept like express for Node JS.

use actix_web::{middleware, App, HttpServer};
use std::env;
#[actix_rt::main]
async fn main() -> std::io::Result<()> {
// init env
dotenv().ok();

// init logger middleware
env::set_var("RUST_LOG", "actix_web=debug,actix_server=info");
env_logger::init();


let app = App::new()
.wrap(middleware::Logger::default());
}

Start server

// server url
let server_url = env::var("SERVER_URL").expect("SERVER_URL is not set in .env file");
// start server
HttpServer::new(move || {
App::new()
.wrap(middleware::Logger::default())
})
.bind(server_url)?
.run()
.await

Init mongo db

Init mongo db connection

use mongodb::{options::ClientOptions, Client};// Parse a connection string into an options struct.
let database_url = env::var("DATABASE_URL").expect("DATABASE_URL is not set in .env file");
let client_options = ClientOptions::parse(&database_url).unwrap();

Access to database

// DATABASE_NAME = local
let database_name = env::var("DATABASE_NAME").expect("DATABASE_NAME is not set in .env file");
let db = client.database(&database_name);

If we use mongo shell commands, this step is equivalent to :

use local
mongo shell commands — connect to database

Access to user collection

// Get a handle to a collection in the database.
let user_collection_name =
env::var("USER_COLLECTION_NAME").expect("USER_COLLECTION_NAME is not set in .env file");
let user_collection = db.collection(&user_collection_name);

If we use mongo shell commands, this step is equivalent to :

coll = db.User
mongo shell commands — use collection

Pass database pool to application so we can access it inside service

use user_service::UserService;

mod user_service;

pub struct ServiceManager {
user: UserService,
}

impl ServiceManager {
pub fn new(user: UserService) -> Self {
ServiceManager { user }
}
}

pub struct AppState {
service_manager: ServiceManager,
}
// start server
HttpServer::new(move || {
let user_service_worker = UserService::new(user_collection.clone());
let service_manager = ServiceManager::new(user_service_worker);

// launch http server
App::new()
.wrap(middleware::Logger::default())
.data(AppState { service_manager })
})
.bind(server_url)?
.run()
.await

More details :

Attach router to http server

mod user_router;// start server
HttpServer::new(move || {
let user_service_worker = UserService::new(user_collection.clone());
let service_manager = ServiceManager::new(user_service_worker);

// launch http server
App::new()
.wrap(middleware::Logger::default())
.data(AppState { service_manager })
.configure(user_router::init)
})
.bind(server_url)?
.run()
.await

User service (dialog with mongo db)

User database utils

1/ User object to mongo document

/// Transform user to mongo db document
fn user_to_document(user: &User) -> Document {
let User {
first_name,
last_name,
user_name,
password,
email,
} = user;
doc! {
"firstName": first_name,
"lastName": last_name,
"username": user_name,
"password": password,
"email": email,
}
}

2/ Build user from fields

///
/// Build user from inputs
///
# Example :
///
///
```
/// let user = build_user(
/// "hela",
/// "ben khalfallah",
/// "hela@hotmail.fr",
/// "helabenkhalfallah",
/// "azerty"
/// )
/// println!("user = {:?}", user);
/// ```
fn build_user(
first_name: String,
last_name: String,
email: String,
user_name: String,
password: String,
) -> User {
User {
first_name,
last_name,
user_name,
password,
email,
}

}

3/ mongo document to User object

///
/// Transform mongo db document to User
///
# Example :
///
///
```
/// let cursor = self.collection.find(None, None).unwrap();
/// for result in cursor {
/// if let Ok(item) = result {
/// data.push(user_from_document(item))
/// }
/// }
/// ```
fn user_from_document(document: Document) -> User {
let mut _first_name = "".to_string();
let mut _last_name = "".to_string();
let mut _email = "".to_string();
let mut _user_name = "".to_string();
let mut _password = "".to_string();
if let Some(&Bson::String(ref first_name)) = document.get("firstName") {
_first_name = first_name.to_string();
}
if let Some(&Bson::String(ref last_name)) = document.get("lastName") {
_last_name = last_name.to_string();
}
if let Some(&Bson::String(ref email)) = document.get("email") {
_email = email.to_string();
}
if let Some(&Bson::String(ref user_name)) = document.get("username") {
_user_name = user_name.to_string();
}
if let Some(&Bson::String(ref password)) = document.get("password") {
_password = password.to_string();
}

build_user(_first_name, _last_name, _email, _user_name, _password)
}

Import required module

use bson::ordered::OrderedDocument;
use bson::{doc, Bson, Document};
use mongodb::results::{DeleteResult, UpdateResult};
use mongodb::{error::Error, results::InsertOneResult, Collection};
use serde::{Deserialize, Serialize};

UserService as an ORM to dialog with mongo db

#[derive(Clone)]
pub struct UserService {
collection: Collection,
}
impl UserService {
pub fn new(collection: Collection) -> UserService {
UserService { collection }
}
}

Define user model

#[derive(Debug, Serialize, Deserialize)]
pub struct User {

pub first_name: String,
pub last_name: String,
pub user_name: String,
pub password: String,
pub email: String,
}

add user

/// Insert user in mongo db (user)
pub fn create(&self, user: &User) -> Result<InsertOneResult, Error> {
self.collection.insert_one(user_to_document(user), None)
}

get all users

/// get all users
pub fn get(&self) -> Result<Vec<User>, Error> {
let cursor = self.collection.find(None, None).unwrap();
let mut data: Vec<User> = Vec::new();

for result in cursor {
if let Ok(item) = result {
data.push(user_from_document(item))
}
}

Ok(data)
}

get user by email

/// Retrieve user by (email)
pub fn get_user_email(&self, email: &String) -> Result<Option<OrderedDocument>, Error> {
self.collection.find_one(doc! { "email": email}, None)
}

update user by email

/// Update existing user in mongo db (email)
pub fn update(&self, user: &User) -> Result<UpdateResult, Error> {
let User {
first_name: _first_name,
last_name: _last_name,
user_name: _user_name,
password: _password,
email,
} = user;
self.collection.update_one(doc! { "email": email}, user_to_document(user), None)
}

delete user by email

/// Delete existing user in mongo db (email)
pub fn delete(&self, email: &String) -> Result<DeleteResult, Error> {
self.collection.delete_one(doc! { "email": email}, None)
}

User router

get all users

app_data is the shared database pool that let us access to the inited service instance and collection in the main function :

use crate::user_service::User;
use actix_web::{delete, get, post, web, HttpResponse, Responder};

#[get("/get-all-users")]
async fn get_all_users(app_data: web::Data<crate::AppState>) -> impl Responder {
let action = app_data.service_manager.user.get();
let result = web::block(move || action).await;
match result {
Ok(result) => HttpResponse::Ok().json(result),
Err(e) => {
println!("Error while getting, {:?}", e);
HttpResponse::InternalServerError().finish()
}
}
}

get user by email

In this service we take email from the path :

#[get("/get-user-email/{email}")]
async fn get_user_email(
app_data: web::Data<crate::AppState>,
email: web::Path<String>,
) -> impl Responder {
let action = app_data.service_manager.user.get_user_email(&email);
let result = web::block(move || action).await;
match result {
Ok(result) => HttpResponse::Ok().json(result),
Err(e) => {
println!("Error while getting, {:?}", e);
HttpResponse::InternalServerError().finish()
}
}
}

add new user

User to insert format is a json object :

#[post("/add-user")]
async fn add_user(
app_data: web::Data<crate::AppState>,
user: web::Json<User>) -> impl Responder {
let action = app_data.service_manager.user.create(&user);
let result = web::block(move || action).await;
match result {
Ok(result) => HttpResponse::Ok().json(result.inserted_id),
Err(e) => {
println!("Error while getting, {:?}", e);
HttpResponse::InternalServerError().finish()
}
}
}

update user by email

#[post("/update-user")]
async fn update_user(
app_data: web::Data<crate::AppState>,
user: web::Json<User>,
) -> impl Responder {
let action = app_data.service_manager.user.update(&user);
let result = web::block(move || action).await;
match result {
Ok(result) => HttpResponse::Ok().json(result.modified_count),
Err(e) => {
println!("Error while getting, {:?}", e);
HttpResponse::InternalServerError().finish()
}
}
}

delete user by email

#[delete("/delete-user")]
async fn delete_user(
app_data: web::Data<crate::AppState>,
user: web::Json<User>,
) -> impl Responder {
let action = app_data.service_manager.user.delete(&user.email);
let result = web::block(move || action).await;
match result {
Ok(result) => HttpResponse::Ok().json(result.deleted_count),
Err(e) => {
println!("Error while getting, {:?}", e);
HttpResponse::InternalServerError().finish()
}
}
}

init Router

pub fn init(cfg: &mut web::ServiceConfig) {
cfg.service(get_all_users);
cfg.service(get_user_email);
cfg.service(add_user);
cfg.service(update_user);
cfg.service(delete_user);
}

Test web-services

get all users
get user by email
add new user
update user
delete user

Project Source Code

Thank you for reading my story.

You can find me at :

Twitter : https://twitter.com/b_k_hela

Github : https://github.com/helabenkhalfallah

--

--

Héla Ben Khalfallah
Héla Ben Khalfallah

Written by Héla Ben Khalfallah

Hello! I'm Héla Ben Khalfallah. I'm a software engineer with a wealth of experience in web solutions, architecture, frontend, FrontendOps, and leadership.

No responses yet