If you’ve been following my Rust series, you may have noticed that we previously built a CRUD API using Axum as the HTTP framework and MongoDB as the database. However, the MongoDB crate has undergone some upgrades, and certain APIs have been deprecated. Notably, the bson-chrono
feature, which was once part of the MongoDB crate, is now included as a feature in the bson
package.
In this article, I will guide you through the process of building a CRUD API—specifically, an API for a feedback application—using Axum as our HTTP framework and MongoDB as our data storage solution.
I understand that when creating APIs in Rust, many developers lean towards SQL-based databases like PostgreSQL. However, MongoDB is also a solid choice, especially for applications that require flexible schema design.
We will use the MongoDB Rust driver to directly interact with the MongoDB server, enabling fast data access without relying on an ODM (Object-Document Mapper) like Mongoose in Node.js.
Related Articles
- Building a Rust API with Unit Testing in Mind
- How to Write Unit Tests for Your Rust API
- How to Add Swagger UI, Redoc and RapiDoc to a Rust API
- JWT Authentication and Authorization in a Rust API using Actix-Web
- How to Write Unit Tests for Your Rust API
- Dockerizing a Rust API Project: SQL Database and pgAdmin
- Deploy Rust App on VPS with GitHub Actions and Docker
Run the Axum MongoDB API on Your Machine
To set up and interact with the various endpoints of the feedback API on your local machine, follow the steps listed below:
- Download or clone the project from its GitHub repository at https://github.com/wpcodevo/feedback-axum-mongodb and open the source code in your preferred code editor.
- Launch a MongoDB server with Docker by running the command
docker-compose up -d
. If Docker is not installed on your machine, you can download and install it from their official website. - After the MongoDB database server is up and running, you can launch the Axum HTTP server by executing the
cargo run
command. This will install any required dependencies and start the Axum development server.
Set Up the Rust Project
Let’s start by setting up our Rust project. Navigate to your desired directory and create a new folder named feedback-axum-mongodb
(feel free to choose a different name).
Open the folder in VS Code or your preferred IDE. In the terminal, run cargo init
to create a new Rust binary project.
After the project is initialized, install the necessary dependencies for building the API by running the following commands in your terminal.
cargo add axum
cargo add tower-http -F 'cors'
cargo add mongodb
cargo add bson -F 'chrono-0_4'
cargo add futures --features async-await --no-default-features
cargo add serde -F derive
cargo add serde_json
cargo add thiserror
cargo add chrono -F serde
cargo add tokio -F full
cargo add dotenv
Launch MongoDB in Docker
We’ll start by launching MongoDB using Docker. If you already have a MongoDB server running locally, feel free to skip this section, but ensure that you add the connection URL to your .env
file.
With that said, create a docker-compose.yml
file and add the following code:
docker-compose.yml
services:
mongo:
image: mongo:latest
container_name: mongo
env_file:
- ./.env
volumes:
- mongo:/data/db
ports:
- '6000:27017'
volumes:
mongo:
Next, create a .env
file in the root directory of your project and add the following environment variables:
.env
MONGO_INITDB_ROOT_USERNAME=admin
MONGO_INITDB_ROOT_PASSWORD=password123
MONGO_INITDB_DATABASE=rust_mongodb
MONGODB_NOTE_COLLECTION=feedbacks
DATABASE_URL=mongodb://admin:password123@localhost:6000/rust_mongodb?authSource=admin
With the environment variables set, run the command docker-compose up -d
to start the MongoDB instance in a Docker container.
Define the Feedback MongoDB Model
Now that we have a running MongoDB database, let’s proceed to create the database model for the feedback application.
To do this, navigate to your src
directory and create a new file named model.rs
. Then, add the following code to the file:
src/model.rs
use chrono::prelude::*;
use mongodb::bson::{self, oid::ObjectId};
use serde::{Deserialize, Serialize};
#[allow(non_snake_case)]
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct FeedbackModel {
#[serde(rename = "_id")]
pub id: ObjectId,
pub name: String,
pub email: String,
pub feedback: String,
pub rating: f32,
pub status: String,
#[serde(with = "bson::serde_helpers::chrono_datetime_as_bson_datetime")]
pub createdAt: DateTime<Utc>,
#[serde(with = "bson::serde_helpers::chrono_datetime_as_bson_datetime")]
pub updatedAt: DateTime<Utc>,
}
Create the HTTP Request Schemas
In API development, it’s always recommended to validate incoming request data to filter out unnecessary fields and ensure the data aligns with the server’s defined schema.
For this simple feedback API, we’ll skip detailed validation but will define structs to ensure the request body and parameters match the expected format. To do this, create a src/schema.rs
file and add the following code:
src/schema.rs
use serde::{Deserialize, Serialize};
#[derive(Deserialize, Debug, Default)]
pub struct FilterOptions {
pub page: Option<usize>,
pub limit: Option<usize>,
}
// #[derive(Deserialize, Debug)]
// pub struct ParamOptions {
// pub id: String,
// }
#[derive(Serialize, Deserialize, Debug)]
pub struct CreateFeedbackSchema {
pub name: String,
pub email: String,
pub feedback: String,
pub rating: f32,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct UpdateFeedbackSchema {
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub email: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub feedback: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub rating: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub status: Option<String>,
}
Create the HTTP Response Schemas
Next, we need to define the structure of the responses we’ll send to the user. To do this, create a src/response.rs
file and add the following code:
src/response.rs
use chrono::{DateTime, Utc};
use serde::Serialize;
#[derive(Serialize)]
pub struct GenericResponse {
pub status: String,
pub message: String,
}
#[allow(non_snake_case)]
#[derive(Serialize, Debug)]
pub struct FeedbackResponse {
pub id: String,
pub name: String,
pub email: String,
pub feedback: String,
pub rating: f32,
pub status: String,
pub createdAt: DateTime<Utc>,
pub updatedAt: DateTime<Utc>,
}
#[derive(Serialize, Debug)]
pub struct FeedbackData {
pub feedback: FeedbackResponse,
}
#[derive(Serialize, Debug)]
pub struct SingleFeedbackResponse {
pub status: &'static str,
pub data: FeedbackData,
}
#[derive(Serialize, Debug)]
pub struct FeedbackListResponse {
pub status: &'static str,
pub results: usize,
pub feedbacks: Vec<FeedbackResponse>,
}
Error Handling with MongoDB and Rust
When working with MongoDB, various errors can occur, and it’s important to handle them gracefully by returning clear, well-formatted messages to users. This approach hides sensitive details that could be exploited by attackers while providing users with understandable explanations of the issue.
To manage these errors effectively, create a src/error.rs
file and add the following code:
src/error.rs
use axum::{http::StatusCode, Json};
use serde::Serialize;
#[derive(thiserror::Error, Debug)]
pub enum MyError {
#[error("MongoDB error")]
MongoError(#[from] mongodb::error::Error),
#[error("duplicate key error: {0}")]
MongoErrorKind(mongodb::error::ErrorKind),
#[error("duplicate key error: {0}")]
MongoDuplicateError(mongodb::error::Error),
#[error("error during mongodb query: {0}")]
MongoQueryError(mongodb::error::Error),
#[error("error serializing BSON")]
MongoSerializeBsonError(#[from] mongodb::bson::ser::Error),
#[error("validation error")]
MongoDataError(#[from] mongodb::bson::document::ValueAccessError),
#[error("invalid ID: {0}")]
InvalidIDError(String),
#[error("Document with ID: {0} not found")]
NotFoundError(String),
}
#[derive(Serialize)]
struct ErrorResponse {
status: &'static str,
message: String,
}
impl Into<(axum::http::StatusCode, Json<serde_json::Value>)> for MyError {
fn into(self) -> (axum::http::StatusCode, Json<serde_json::Value>) {
let (status, error_response) = match self {
MyError::MongoErrorKind(e) => (
StatusCode::INTERNAL_SERVER_ERROR,
ErrorResponse {
status: "error",
message: format!("MongoDB error kind: {}", e),
},
),
MyError::MongoDuplicateError(_) => (
StatusCode::CONFLICT,
ErrorResponse {
status: "fail",
message: "Feedback with that text already exists".to_string(),
},
),
MyError::InvalidIDError(id) => (
StatusCode::BAD_REQUEST,
ErrorResponse {
status: "fail",
message: format!("invalid ID: {}", id),
},
),
MyError::NotFoundError(id) => (
StatusCode::NOT_FOUND,
ErrorResponse {
status: "fail",
message: format!("Feedback with ID: {} not found", id),
},
),
MyError::MongoError(e) => (
StatusCode::INTERNAL_SERVER_ERROR,
ErrorResponse {
status: "error",
message: format!("MongoDB error: {}", e),
},
),
MyError::MongoQueryError(e) => (
StatusCode::INTERNAL_SERVER_ERROR,
ErrorResponse {
status: "error",
message: format!("MongoDB error: {}", e),
},
),
MyError::MongoSerializeBsonError(e) => (
StatusCode::INTERNAL_SERVER_ERROR,
ErrorResponse {
status: "error",
message: format!("MongoDB error: {}", e),
},
),
MyError::MongoDataError(e) => (
StatusCode::INTERNAL_SERVER_ERROR,
ErrorResponse {
status: "error",
message: format!("MongoDB error: {}", e),
},
),
};
(status, Json(serde_json::to_value(error_response).unwrap()))
}
}
impl From<MyError> for (StatusCode, ErrorResponse) {
fn from(err: MyError) -> (StatusCode, ErrorResponse) {
err.into()
}
}
Connect the Axum Server to MongoDB
At this stage, we’ve set up error handling and created our MongoDB model. The next step is to connect our Axum server to the running MongoDB instance.
To keep things organized, we’ll place all database-related code in a db.rs
file. Navigate to the src
directory and create a new file named db.rs
. Add the following code to it:
src/db.rs
use crate::error::MyError;
use crate::model::FeedbackModel;
use mongodb::bson::Document;
use mongodb::{ options::ClientOptions, Client, Collection};
#[derive(Clone, Debug)]
pub struct DB {
pub feedback_collection: Collection<FeedbackModel>,
pub collection: Collection<Document>,
}
type Result<T> = std::result::Result<T, MyError>;
impl DB {
pub async fn init() -> Result<Self> {
let mongodb_uri = std::env::var("DATABASE_URL").expect("DATABASE_URL must be set.");
let database_name =
std::env::var("MONGO_INITDB_DATABASE").expect("MONGO_INITDB_DATABASE must be set.");
let collection_name =
std::env::var("MONGODB_NOTE_COLLECTION").expect("MONGODB_NOTE_COLLECTION must be set.");
let mut client_options = ClientOptions::parse(mongodb_uri).await?;
client_options.app_name = Some(database_name.to_string());
let client = Client::with_options(client_options)?;
let database = client.database(database_name.as_str());
let feedback_collection = database.collection(collection_name.as_str());
let collection = database.collection::<Document>(collection_name.as_str());
println!("✅ Database connected successfully");
Ok(Self {
feedback_collection,
collection,
})
}
}
We’ve defined an init
function to handle the connection to the MongoDB instance and to create the feedback collection in the database.
Now, we need to call the init
function within the main
function to establish the connection to the MongoDB server.
src/main.rs
mod db;
mod error;
mod model;
use std::sync::Arc;
use axum::Router;
use db::DB;
use dotenv::dotenv;
use error::MyError;
pub struct AppState {
db: DB,
}
#[tokio::main]
async fn main() -> Result<(), MyError> {
dotenv().ok();
let db = DB::init().await?;
let app = Router::new().with_state(Arc::new(AppState{db: db.clone()}));
println!("🚀 Server started successfully");
let listener = tokio::net::TcpListener::bind("0.0.0.0:8000").await.unwrap();
axum::serve(listener, app).await.unwrap();
Ok(())
}
Transform MongoDB Document to Rust Struct
When data is retrieved from MongoDB, the MongoDB driver returns it in BSON format. To make the data usable in our application, we must convert it into a Rust type. The simplest approach is to deserialize the BSON into a Rust struct.
To do this, create a src/db.rs
file and add the following code:
src/db.rs
use bson::Document;
use mongodb::Collection;
use crate::{error::MyError, model::FeedbackModel, response::FeedbackResponse};
#[derive(Clone, Debug)]
pub struct DB {
pub feedback_collection: Collection<FeedbackModel>,
pub collection: Collection<Document>,
}
type Result<T> = std::result::Result<T, MyError>;
impl DB {
fn doc_to_feedback(&self, feedback: &FeedbackModel) -> Result<FeedbackResponse> {
let feedback_response = FeedbackResponse {
id: feedback.id.to_hex(),
name: feedback.name.to_owned(),
email: feedback.email.to_owned(),
feedback: feedback.feedback.to_owned(),
rating: feedback.rating.to_owned(),
status: feedback.status.to_owned(),
createdAt: feedback.createdAt,
updatedAt: feedback.updatedAt,
};
Ok(feedback_response)
}
}
Perform MongoDB CRUD Operations
Now that our application is connected to MongoDB, it’s time to perform CRUD operations on the database. To achieve this, we’ll create higher-level functions that utilize the lower-level CRUD operations provided by the MongoDB driver.
Fetch All Documents
Let’s start with the READ operation, which allows us to retrieve multiple documents from MongoDB. For this, we use the .find()
method available on the collection. This method returns a cursor, which we iterate through to access individual documents. Below is the code demonstrating this:
src/db.rs
#[derive(Clone, Debug)]
pub struct DB {
pub feedback_collection: Collection<FeedbackModel>,
pub collection: Collection<Document>,
}
type Result<T> = std::result::Result<T, MyError>;
impl DB {
pub async fn fetch_feedbacks(&self, limit: i64, page: i64) -> Result<FeedbackListResponse> {
let mut cursor = self
.feedback_collection
.find(doc! {})
.limit(limit)
.skip(u64::try_from((page - 1) * limit).unwrap())
.await
.map_err(MongoQueryError)?;
let mut json_result: Vec<FeedbackResponse> = Vec::new();
while let Some(doc) = cursor.next().await {
json_result.push(self.doc_to_feedback(&doc.unwrap())?);
}
Ok(FeedbackListResponse {
status: "success",
results: json_result.len(),
feedbacks: json_result,
})
}
}
Additionally, we use the .limit()
and .skip()
methods to ensure that we don’t return all the documents in the collection at once.
Insert a Document
Let’s move on to the CREATE operation, which involves inserting a document into the collection. We’ll also add an index to ensure no duplicate feedback is added to the collection. Afterwards, we’ll use the .insert_one()
method to add the new document to the collection.
src/db.rs
#[derive(Clone, Debug)]
pub struct DB {
pub feedback_collection: Collection<FeedbackModel>,
pub collection: Collection<Document>,
}
type Result<T> = std::result::Result<T, MyError>;
impl DB {
pub async fn create_feedback(
&self,
body: &CreateFeedbackSchema,
) -> Result<SingleFeedbackResponse> {
let status = String::from("pending");
let document = self.create_feedback_document(body, status)?;
let options = IndexOptions::builder().unique(true).build();
let index = IndexModel::builder()
.keys(doc! {"feedback": 1})
.options(options)
.build();
match self.feedback_collection.create_index(index).await {
Ok(_) => {}
Err(e) => return Err(MongoQueryError(e)),
};
let insert_result = match self.collection.insert_one(&document).await {
Ok(result) => result,
Err(e) => {
if e.to_string()
.contains("E11000 duplicate key error collection")
{
return Err(MongoDuplicateError(e));
}
return Err(MongoQueryError(e));
}
};
let new_id = insert_result
.inserted_id
.as_object_id()
.expect("issue with new _id");
let feedback_doc = match self
.feedback_collection
.find_one(doc! {"_id": new_id})
.await
{
Ok(Some(doc)) => doc,
Ok(None) => return Err(NotFoundError(new_id.to_string())),
Err(e) => return Err(MongoQueryError(e)),
};
Ok(SingleFeedbackResponse {
status: "success",
data: FeedbackData {
feedback: self.doc_to_feedback(&feedback_doc)?,
},
})
}
fn create_feedback_document(
&self,
body: &CreateFeedbackSchema,
status: String,
) -> Result<bson::Document> {
let serialized_data = bson::to_bson(body).map_err(MongoSerializeBsonError)?;
let document = serialized_data.as_document().unwrap();
let datetime = Utc::now();
let mut doc_with_dates = doc! {
"createdAt": datetime,
"updatedAt": datetime,
"status": status };
doc_with_dates.extend(document.clone());
Ok(doc_with_dates)
}
}
Fetch a Document
Next, let’s handle the second use case of the READ operation, which involves retrieving a single document from the collection. We will use the .find_one()
method, passing in the ID of the document we wish to retrieve.
src/db.rs
#[derive(Clone, Debug)]
pub struct DB {
pub feedback_collection: Collection<FeedbackModel>,
pub collection: Collection<Document>,
}
type Result<T> = std::result::Result<T, MyError>;
impl DB {
pub async fn get_feedback(&self, id: &str) -> Result<SingleFeedbackResponse> {
let oid = ObjectId::from_str(id).map_err(|_| InvalidIDError(id.to_owned()))?;
let feedback_doc = self
.feedback_collection
.find_one(doc! {"_id":oid })
.await
.map_err(MongoQueryError)?;
match feedback_doc {
Some(doc) => {
let feedback = self.doc_to_feedback(&doc)?;
Ok(SingleFeedbackResponse {
status: "success",
data: FeedbackData { feedback },
})
}
None => Err(NotFoundError(id.to_string())),
}
}
}
Edit a Document
When updating a document, there are two common HTTP methods: PUT and PATCH. PUT is used to update an entire document, replacing it completely, while PATCH allows partial updates to specific fields.
For our API, we’ll use the PATCH approach to provide more flexibility. MongoDB’s Rust driver offers a find_one_and_update()
method, which is ideal for updating specific fields in a document. Here’s how you can implement it:
src/db.rs
#[derive(Clone, Debug)]
pub struct DB {
pub feedback_collection: Collection<FeedbackModel>,
pub collection: Collection<Document>,
}
type Result<T> = std::result::Result<T, MyError>;
impl DB {
pub async fn edit_feedback(
&self,
id: &str,
body: &UpdateFeedbackSchema,
) -> Result<SingleFeedbackResponse> {
let oid = ObjectId::from_str(id).map_err(|_| InvalidIDError(id.to_owned()))?;
let update = doc! {
"$set": bson::to_document(body).map_err(MongoSerializeBsonError)?,
};
if let Some(doc) = self
.feedback_collection
.find_one_and_update(doc! {"_id": oid}, update)
.return_document(ReturnDocument::After)
.await
.map_err(MongoQueryError)?
{
let feedback = self.doc_to_feedback(&doc)?;
let feedback_response = SingleFeedbackResponse {
status: "success",
data: FeedbackData { feedback },
};
Ok(feedback_response)
} else {
Err(NotFoundError(id.to_string()))
}
}
}
We used the .return_document()
method to retrieve the updated document from the collection after the update operation.
Delete a Document
Let’s wrap up by implementing the DELETE operation. First, we need to parse the ID provided in the URL parameter to ensure it’s a valid MongoDB ObjectID. Then, we create a filter and use the .delete_one()
method to remove the document that matches the query from the collection.
src/db.rs
#[derive(Clone, Debug)]
pub struct DB {
pub feedback_collection: Collection<FeedbackModel>,
pub collection: Collection<Document>,
}
type Result<T> = std::result::Result<T, MyError>;
impl DB {
pub async fn delete_feedback(&self, id: &str) -> Result<()> {
let oid = ObjectId::from_str(id).map_err(|_| InvalidIDError(id.to_owned()))?;
let filter = doc! {"_id": oid };
let result = self
.collection
.delete_one(filter)
.await
.map_err(MongoQueryError)?;
match result.deleted_count {
0 => Err(NotFoundError(id.to_string())),
_ => Ok(()),
}
}
}
Complete MongoDB CRUD Code
Below is the complete code for the higher-level CRUD functions we implemented in the previous sections.
src/db.rs
use crate::error::MyError;
use crate::response::{
FeedbackData, FeedbackListResponse, FeedbackResponse, SingleFeedbackResponse,
};
use crate::{
error::MyError::*, model::FeedbackModel, schema::CreateFeedbackSchema,
schema::UpdateFeedbackSchema,
};
use chrono::prelude::*;
use futures::StreamExt;
use mongodb::bson::{doc, oid::ObjectId, Document};
use mongodb::options::{IndexOptions, ReturnDocument};
use mongodb::{bson, options::ClientOptions, Client, Collection, IndexModel};
use std::str::FromStr;
#[derive(Clone, Debug)]
pub struct DB {
pub feedback_collection: Collection<FeedbackModel>,
pub collection: Collection<Document>,
}
type Result<T> = std::result::Result<T, MyError>;
impl DB {
pub async fn init() -> Result<Self> {
let mongodb_uri = std::env::var("DATABASE_URL").expect("DATABASE_URL must be set.");
let database_name =
std::env::var("MONGO_INITDB_DATABASE").expect("MONGO_INITDB_DATABASE must be set.");
let collection_name =
std::env::var("MONGODB_NOTE_COLLECTION").expect("MONGODB_NOTE_COLLECTION must be set.");
let mut client_options = ClientOptions::parse(mongodb_uri).await?;
client_options.app_name = Some(database_name.to_string());
let client = Client::with_options(client_options)?;
let database = client.database(database_name.as_str());
let feedback_collection = database.collection(collection_name.as_str());
let collection = database.collection::<Document>(collection_name.as_str());
println!("✅ Database connected successfully");
Ok(Self {
feedback_collection,
collection,
})
}
pub async fn fetch_feedbacks(&self, limit: i64, page: i64) -> Result<FeedbackListResponse> {
let mut cursor = self
.feedback_collection
.find(doc! {})
.limit(limit)
.skip(u64::try_from((page - 1) * limit).unwrap())
.await
.map_err(MongoQueryError)?;
let mut json_result: Vec<FeedbackResponse> = Vec::new();
while let Some(doc) = cursor.next().await {
json_result.push(self.doc_to_feedback(&doc.unwrap())?);
}
Ok(FeedbackListResponse {
status: "success",
results: json_result.len(),
feedbacks: json_result,
})
}
pub async fn create_feedback(
&self,
body: &CreateFeedbackSchema,
) -> Result<SingleFeedbackResponse> {
let status = String::from("pending");
let document = self.create_feedback_document(body, status)?;
let options = IndexOptions::builder().unique(true).build();
let index = IndexModel::builder()
.keys(doc! {"feedback": 1})
.options(options)
.build();
match self.feedback_collection.create_index(index).await {
Ok(_) => {}
Err(e) => return Err(MongoQueryError(e)),
};
let insert_result = match self.collection.insert_one(&document).await {
Ok(result) => result,
Err(e) => {
if e.to_string()
.contains("E11000 duplicate key error collection")
{
return Err(MongoDuplicateError(e));
}
return Err(MongoQueryError(e));
}
};
let new_id = insert_result
.inserted_id
.as_object_id()
.expect("issue with new _id");
let feedback_doc = match self
.feedback_collection
.find_one(doc! {"_id": new_id})
.await
{
Ok(Some(doc)) => doc,
Ok(None) => return Err(NotFoundError(new_id.to_string())),
Err(e) => return Err(MongoQueryError(e)),
};
Ok(SingleFeedbackResponse {
status: "success",
data: FeedbackData {
feedback: self.doc_to_feedback(&feedback_doc)?,
},
})
}
pub async fn get_feedback(&self, id: &str) -> Result<SingleFeedbackResponse> {
let oid = ObjectId::from_str(id).map_err(|_| InvalidIDError(id.to_owned()))?;
let feedback_doc = self
.feedback_collection
.find_one(doc! {"_id":oid })
.await
.map_err(MongoQueryError)?;
match feedback_doc {
Some(doc) => {
let feedback = self.doc_to_feedback(&doc)?;
Ok(SingleFeedbackResponse {
status: "success",
data: FeedbackData { feedback },
})
}
None => Err(NotFoundError(id.to_string())),
}
}
pub async fn edit_feedback(
&self,
id: &str,
body: &UpdateFeedbackSchema,
) -> Result<SingleFeedbackResponse> {
let oid = ObjectId::from_str(id).map_err(|_| InvalidIDError(id.to_owned()))?;
let update = doc! {
"$set": bson::to_document(body).map_err(MongoSerializeBsonError)?,
};
if let Some(doc) = self
.feedback_collection
.find_one_and_update(doc! {"_id": oid}, update)
.return_document(ReturnDocument::After)
.await
.map_err(MongoQueryError)?
{
let feedback = self.doc_to_feedback(&doc)?;
let feedback_response = SingleFeedbackResponse {
status: "success",
data: FeedbackData { feedback },
};
Ok(feedback_response)
} else {
Err(NotFoundError(id.to_string()))
}
}
pub async fn delete_feedback(&self, id: &str) -> Result<()> {
let oid = ObjectId::from_str(id).map_err(|_| InvalidIDError(id.to_owned()))?;
let filter = doc! {"_id": oid };
let result = self
.collection
.delete_one(filter)
.await
.map_err(MongoQueryError)?;
match result.deleted_count {
0 => Err(NotFoundError(id.to_string())),
_ => Ok(()),
}
}
fn doc_to_feedback(&self, feedback: &FeedbackModel) -> Result<FeedbackResponse> {
let feedback_response = FeedbackResponse {
id: feedback.id.to_hex(),
name: feedback.name.to_owned(),
email: feedback.email.to_owned(),
feedback: feedback.feedback.to_owned(),
rating: feedback.rating.to_owned(),
status: feedback.status.to_owned(),
createdAt: feedback.createdAt,
updatedAt: feedback.updatedAt,
};
Ok(feedback_response)
}
fn create_feedback_document(
&self,
body: &CreateFeedbackSchema,
status: String,
) -> Result<bson::Document> {
let serialized_data = bson::to_bson(body).map_err(MongoSerializeBsonError)?;
let document = serialized_data.as_document().unwrap();
let datetime = Utc::now();
let mut doc_with_dates = doc! {
"createdAt": datetime,
"updatedAt": datetime,
"status": status };
doc_with_dates.extend(document.clone());
Ok(doc_with_dates)
}
}
Process CRUD Requests with Axum
With the database CRUD functions defined, we can now integrate them into our route handlers to process the incoming CRUD requests to our server.
Fetch All Feedbacks
Let’s create an Axum route handler to handle GET requests for retrieving multiple feedback entries. By default, the handler will return the first 10 items, but users can specify limit
and page
parameters in the request to adjust pagination.
src/handler.rs
pub async fn feedback_list_handler(
opts: Option<Query<FilterOptions>>,
State(app_state): State<Arc<AppState>>,
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
let Query(opts) = opts.unwrap_or_default();
let limit = opts.limit.unwrap_or(10) as i64;
let page = opts.page.unwrap_or(1) as i64;
match app_state
.db
.fetch_feedbacks(limit, page)
.await
.map_err(MyError::from)
{
Ok(res) => Ok(Json(res)),
Err(e) => Err(e.into()),
}
}
Add a Feedback
Next, let’s handle the POST request to add new feedback to our database. We’ll achieve this by invoking the db.create_feedback()
method, and then we will return the newly created document in the JSON response.
src/handler.rs
pub async fn create_feedback_handler(
State(app_state): State<Arc<AppState>>,
Json(body): Json<CreateFeedbackSchema>,
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
match app_state.db.create_feedback(&body).await.map_err(MyError::from) {
Ok(res) => Ok((StatusCode::CREATED, Json(res))),
Err(e) => Err(e.into()),
}
}
Fetch a Feedback
The next step is to handle another READ request, where the handler will return a single document in the JSON response.
src/handler.rs
pub async fn get_feedback_handler(
Path(id): Path<String>,
State(app_state): State<Arc<AppState>>,
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
match app_state.db.get_feedback(&id).await.map_err(MyError::from) {
Ok(res) => Ok(Json(res)),
Err(e) => Err(e.into()),
}
}
Edit a Feedback
Next, we will process the PATCH request to partially update the document matching the provided ID in the request parameter. The updated document will be returned in the JSON response.
src/handler.rs
pub async fn edit_feedback_handler(
Path(id): Path<String>,
State(app_state): State<Arc<AppState>>,
Json(body): Json<UpdateFeedbackSchema>,
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
match app_state
.db
.edit_feedback(&id, &body)
.await
.map_err(MyError::from)
{
Ok(res) => Ok(Json(res)),
Err(e) => Err(e.into()),
}
}
Delete a Feedback
Finally, let’s handle the DELETE request by invoking the db.delete_feedback()
method to remove the document matching the provided ID from the collection. Upon a successful deletion, a 204 status code will be returned to the user, indicating that the operation was completed without returning any content.
src/handler.rs
pub async fn delete_feedback_handler(
Path(id): Path<String>,
State(app_state): State<Arc<AppState>>,
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
match app_state.db.delete_feedback(&id).await.map_err(MyError::from) {
Ok(_) => Ok(StatusCode::NO_CONTENT),
Err(e) => Err(e.into()),
}
}
Complete Axum CRUD Code
Here’s the complete code for handling the CRUD HTTP requests:
src/handler.rs
use std::sync::Arc;
use axum::{
extract::{Path, Query, State},
http::StatusCode,
response::IntoResponse,
Json,
};
use crate::{
error::MyError,
schema::{CreateFeedbackSchema, FilterOptions, UpdateFeedbackSchema},
AppState,
};
pub async fn health_checker_handler() -> impl IntoResponse {
const MESSAGE: &str = "Feedback API in Rust using Axum Framework and MongoDB";
let json_response = serde_json::json!({
"status": "success",
"message": MESSAGE
});
Json(json_response)
}
pub async fn feedback_list_handler(
opts: Option<Query<FilterOptions>>,
State(app_state): State<Arc<AppState>>,
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
let Query(opts) = opts.unwrap_or_default();
let limit = opts.limit.unwrap_or(10) as i64;
let page = opts.page.unwrap_or(1) as i64;
match app_state
.db
.fetch_feedbacks(limit, page)
.await
.map_err(MyError::from)
{
Ok(res) => Ok(Json(res)),
Err(e) => Err(e.into()),
}
}
pub async fn create_feedback_handler(
State(app_state): State<Arc<AppState>>,
Json(body): Json<CreateFeedbackSchema>,
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
match app_state.db.create_feedback(&body).await.map_err(MyError::from) {
Ok(res) => Ok((StatusCode::CREATED, Json(res))),
Err(e) => Err(e.into()),
}
}
pub async fn get_feedback_handler(
Path(id): Path<String>,
State(app_state): State<Arc<AppState>>,
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
match app_state.db.get_feedback(&id).await.map_err(MyError::from) {
Ok(res) => Ok(Json(res)),
Err(e) => Err(e.into()),
}
}
pub async fn edit_feedback_handler(
Path(id): Path<String>,
State(app_state): State<Arc<AppState>>,
Json(body): Json<UpdateFeedbackSchema>,
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
match app_state
.db
.edit_feedback(&id, &body)
.await
.map_err(MyError::from)
{
Ok(res) => Ok(Json(res)),
Err(e) => Err(e.into()),
}
}
pub async fn delete_feedback_handler(
Path(id): Path<String>,
State(app_state): State<Arc<AppState>>,
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
match app_state.db.delete_feedback(&id).await.map_err(MyError::from) {
Ok(_) => Ok(StatusCode::NO_CONTENT),
Err(e) => Err(e.into()),
}
}
Create Axum CRUD Routes
With our route handlers or controllers defined, we can now proceed to invoke them. Below is the code to accomplish this:
src/route.rs
use std::sync::Arc;
use axum::{
routing::{get, post},
Router,
};
use crate::{
handler::{
create_feedback_handler, delete_feedback_handler, edit_feedback_handler, get_feedback_handler,
health_checker_handler, feedback_list_handler,
},
AppState,
};
pub fn create_router(app_state: Arc<AppState>) -> Router {
Router::new()
.route("/api/healthchecker", get(health_checker_handler))
.route("/api/feedbacks/", post(create_feedback_handler))
.route("/api/feedbacks", get(feedback_list_handler))
.route(
"/api/feedbacks/:id",
get(get_feedback_handler)
.patch(edit_feedback_handler)
.delete(delete_feedback_handler),
)
.with_state(app_state)
}
We used the .with_state()
method to pass in our application state, allowing our route handlers to access the MongoDB database instance.
Register the API Router and Set Up CORS
Let’s conclude by registering the API router and configuring CORS for the server.
src/main.rs
mod db;
mod error;
mod handler;
mod model;
mod response;
mod route;
mod schema;
use std::sync::Arc;
use axum::http::{
header::{ACCEPT, AUTHORIZATION, CONTENT_TYPE},
HeaderValue, Method,
};
use db::DB;
use dotenv::dotenv;
use error::MyError;
use route::create_router;
use tower_http::cors::CorsLayer;
pub struct AppState {
db: DB,
}
#[tokio::main]
async fn main() -> Result<(), MyError> {
dotenv().ok();
let db = DB::init().await?;
let cors = CorsLayer::new()
.allow_origin("http://localhost:3000".parse::<HeaderValue>().unwrap())
.allow_methods([Method::GET, Method::POST, Method::PATCH, Method::DELETE])
.allow_credentials(true)
.allow_headers([AUTHORIZATION, ACCEPT, CONTENT_TYPE]);
let app = create_router(Arc::new(AppState { db: db.clone() })).layer(cors);
println!("🚀 Server started successfully");
let listener = tokio::net::TcpListener::bind("0.0.0.0:8000").await.unwrap();
axum::serve(listener, app).await.unwrap();
Ok(())
}
If you’ve followed all the previous sections correctly, you can start the Axum development server if you haven’t already. To ensure that our API is functioning as expected, let’s execute the first CRUD operation to check if a document is successfully added to our database.
Open your API client—I’ll be using the Postman extension in VS Code. Enter the URL http://localhost:8000/api/feedbacks/
and include the following JSON object in the request body.
{
"name": "John Smith",
"email": "johnsmith@gmail.com",
"feedback": "Your channel is the best thing that ever happended to me",
"rating": 4.5,
"status": "active"
}
Once you’ve completed the setup, send the request. After a few seconds, you should receive a 201 response containing the newly created data.
Conclusion
And we are done! Congratulations on reaching this point. In this tutorial, you learned how to build a CRUD API in Rust using MongoDB as the database and Axum for handling HTTP requests.
I hope you found this guide both helpful and enjoyable. If you have any questions or feedback, please feel free to leave them in the comments section. Thank you for reading!