Join us at MongoDB.local London on 7 May to unlock new possibilities for your data. Use WEB50 to save 50%.
Register now >
Docs Menu
Docs Home
/ /

CRUD Web App with Rocket

In this tutorial, you can learn how to create a Rust web application by using the Rocket web framework. The Rust driver allows you to leverage features such as memory management, lifetimes, and database pooling to improve your application's performance.

After you complete this tutorial, you will have a web application with routes to perform CRUD operations.

Tip

Complete App

To view the complete sample app for this tutorial, see the web-app-tutorial folder on GitHub.

Ensure you have Rust 1.74 or later, and Cargo, the Rust package manager, installed in your development environment.

For information about how to install Rust and Cargo, see the official Rust guide on downloading and installing Rust.

You must also set up a MongoDB Atlas cluster. To learn how to create a cluster, see the Create a MongoDB Deployment step of the Quick Start guide. Save your connection string in a safe location to use later in the tutorial.

1

Run the following commands to create and enter the rust-crud-app project directory:

cargo new rust-crud-app
cd rust-crud-app
2

Run the following command to add the Rust driver:

cargo add mongodb

Verify that your Cargo.toml file contains the following entry for the Rust driver:

[dependencies]
mongodb = "3.6.0"
3

When using the Rust driver, you must select either the synchronous or asynchronous runtime. This tutorial uses the asynchronous runtime, which is more suitable for building APIs.

The driver runs with the async tokio runtime by default.

To learn more about the available runtimes, see the Asynchronous and Synchronous APIs guide.

4

Access the Atlas UI, and then select the Collections tab in your cluster settings. Select the + Create Database button. In the modal, create a database called bread, and within it, a collection called recipes.

5

Select the INSERT DOCUMENT button and paste the contents of the bread_data.json file in the sample app.

After you insert the data, you can see the sample documents in the recipes collection.

6

Open your IDE and enter your project directory. Run the following command from your project root to install the Rocket web framework:

cargo add -F json rocket

Verify that the dependencies list in your Cargo.toml file contains an entry for rocket.

You must also add a crate developed by Rocket that allows you to use a wrapper to manage a collection pool for the async connections made by your MongoDB client. This crate allows you to parameterize your MongoDB databases and collections and have each app function receive its own connection to use.

Run the following command to add the rocket_db_pools crate:

cargo add -F mongodb rocket_db_pools

Verify that the dependencies list in your Cargo.toml file contains an entry for rocket_db_pools that contains a feature flag for mongodb.

7

To configure Rocket to use the bread database, create a file called Rocket.toml in your project root. Rocket looks for this file to read configuration settings. You can also store your MongoDB connection string in this file.

Paste the following configuration into the Rocket.toml file:

[default.databases.db]
url = "<connection string>"

To learn more about configuring Rocket, see Configuration in the Rocket documentation.

8

Before you begin writing the API, learn about the structure of a simple Rocket app and create the corresponding files in your application.

The following diagram demonstrates the file structure that your Rocket app must have and explains the function of each file:

.
├── Cargo.lock # Dependency info
├── Cargo.toml # Project and dependency info
├── Rocket.toml # Rocket configuration
└── src # Directory for all app code
├── db.rs # Establishes database connection
├── main.rs # Starts the web app
├── models.rs # Organizes data
└── routes.rs # Stores API routes

Create the src directory and the files it contains, according to the preceding diagram. At this point, the files can be empty.

9

Paste the following code into the db.rs file:

src/db.rs
use rocket_db_pools::{mongodb::Client, Database};
#[derive(Database)]
#[database("db")]
pub struct MainDatabase(Client);

You must also attach the database struct to your Rocket instance. Copy the following template code into your main.rs file:

src/main.rs
mod db;
mod models;
mod routes;
use rocket::{launch, routes};
use rocket_db_pools::Database;
#[launch]
fn rocket() -> _ {
rocket::build()
.attach(db::MainDatabase::init())
// Paste mount() call below
}

Your IDE might raise an error that the function body is incomplete. You can ignore this error, because you will add the mount() call in a later step.

10

Defining consistent and useful structs to represent your data is important for maintaining type safety and reducing runtime errors.

In your models.rs file, define a Recipe struct that represents a recipe to bake bread:

src/models.rs
use mongodb::bson::oid::ObjectId;
use rocket::serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
#[serde(crate = "rocket::serde")]
pub struct Recipe {
#[serde(rename = "_id", skip_serializing_if = "Option::is_none")]
pub id: Option<ObjectId>,
pub title: String,
pub ingredients: Vec<String>,
pub temperature: u32,
pub bake_time: u32,
}
11

Routing allows the program to direct the request to the proper endpoint to send or receive the data. The file routes.rs stores all the routes defined in the API.

Copy the following template code into your routes.rs file:

src/routes.rs
use crate::db::MainDatabase;
use crate::models::Recipe;
use mongodb::bson::doc;
use mongodb::bson::oid::ObjectId;
use rocket::{
delete, futures::TryStreamExt, get, http::Status, post, put, response::status,
serde::json::Json,
};
use rocket_db_pools::Connection;
use serde_json::{json, Value};
// Paste index route below
// Paste get_recipes route below
// Paste get_recipe route below
// Paste update_recipe route below
// Paste delete_recipe route below
// Paste create_recipe route below

Paste the following index route under the // Paste index route below comment:

#[get("/")]
pub fn index() -> Json<Value> {
Json(json!({"status": "It is time to make some bread!!!"}))
}

Before you write the remaining routes, add the routes to the main launch function for Rocket.

In your main.rs file, paste the following code under the // Paste mount() call below comment:

.mount(
"/",
routes![
routes::index,
routes::get_recipes,
routes::create_recipe,
routes::get_recipe,
routes::update_recipe,
routes::delete_recipe,
],
)
12

In your app, you must implement error handling and custom responses to deal with unexpected outcomes of your CRUD operations.

Install the serde_json crate by running the following command:

cargo add serde_json

This crate includes the Value enum that represents a JSON value.

You can specify that your routes return HTTP status codes by using Rocket's status::Custom structs, which allow you to specify the HTTP status code and any custom data to return. The following step describes how to write routes that return the status::Custom type.

13

When you attempt to create data in MongoDB, there are two possible outcomes:

  • Document is successfully created, so your app returns HTTP 201.

  • Error occurred during insertion, so your app returns HTTP 400.

Paste the following create_recipe() route under the // Paste create_recipe route below comment in your routes.rs file:

src/routes.rs
#[post("/recipes", data = "<data>", format = "json")]
pub async fn create_recipe(
db: Connection<MainDatabase>,
data: Json<Recipe>,
) -> status::Custom<Json<Value>> {
if let Ok(res) = db
.database("bread")
.collection::<Recipe>("recipes")
.insert_one(data.into_inner(), None)
.await
{
if let Some(id) = res.inserted_id.as_object_id() {
return status::Custom(
Status::Created,
Json(
json!({"status": "success", "message": format!("Recipe ({}) created successfully", id.to_string())}),
),
);
}
}
status::Custom(
Status::BadRequest,
Json(json!({"status": "error", "message":"Recipe could not be created"})),
)
}

When you attempt to read data from MongoDB, there are two possible outcomes:

  • Return the vector of matching documents.

  • Return an empty vector, because there are no matching documents or because an error occurred.

Because of these expected outcomes, paste the following get_recipes() route under the // Paste get_recipes route below comment in your routes.rs file:

src/routes.rs
#[get("/recipes", format = "json")]
pub async fn get_recipes(db: Connection<MainDatabase>) -> Json<Vec<Recipe>> {
let recipes = db
.database("bread")
.collection("recipes")
.find(None, None)
.await;
if let Ok(r) = recipes {
if let Ok(collected) = r.try_collect::<Vec<Recipe>>().await {
return Json(collected);
}
}
return Json(vec![]);
}

Paste the following get_recipe() route under the // Paste get_recipe route below comment:

src/routes.rs
#[get("/recipes/<id>", format = "json")]
pub async fn get_recipe(
db: Connection<MainDatabase>,
id: &str,
) -> status::Custom<Json<Value>> {
let b_id = ObjectId::parse_str(id);
if b_id.is_err() {
return status::Custom(
Status::BadRequest,
Json(json!({"status": "error", "message":"Recipe ID is invalid"})),
);
}
if let Ok(Some(recipe)) = db
.database("bread")
.collection::<Recipe>("recipes")
.find_one(doc! {"_id": b_id.unwrap()}, None)
.await
{
return status::Custom(
Status::Ok,
Json(json!({"status": "success", "data": recipe})),
);
}
return status::Custom(
Status::NotFound,
Json(json!({"status": "success", "message": "Recipe not found"})),
);
}

This route retrieves a single document by its _id value.

Paste the following update_recipe() route under the // Paste update_recipe route below comment:

src/routes.rs
#[put("/recipes/<id>", data = "<data>", format = "json")]
pub async fn update_recipe(
db: Connection<MainDatabase>,
data: Json<Recipe>,
id: &str,
) -> status::Custom<Json<Value>> {
let b_id = ObjectId::parse_str(id);
if b_id.is_err() {
return status::Custom(
Status::BadRequest,
Json(json!({"status": "error", "message":"Recipe ID is invalid"})),
);
}
if let Ok(_) = db
.database("bread")
.collection::<Recipe>("recipes")
.update_one(
doc! {"_id": b_id.as_ref().unwrap()},
doc! {"$set": mongodb::bson::to_document(&data.into_inner()).unwrap()},
None,
)
.await
{
return status::Custom(
Status::Created,
Json(
json!({"status": "success", "message": format!("Recipe ({}) updated successfully", b_id.unwrap())}),
),
);
};
status::Custom(
Status::BadRequest,
Json(
json!({"status": "success", "message": format!("Recipe ({}) could not be updated successfully", b_id.unwrap())}),
),
)
}

This route updates a single document by its _id value.

Paste the following delete_recipe() route after the // Paste delete_recipe route below comment:

src/routes.rs
#[delete("/recipes/<id>")]
pub async fn delete_recipe(
db: Connection<MainDatabase>,
id: &str,
) -> status::Custom<Json<Value>> {
let b_id = ObjectId::parse_str(id);
if b_id.is_err() {
return status::Custom(
Status::BadRequest,
Json(json!({"status": "error", "message":"Recipe ID is invalid"})),
);
}
if db
.database("bread")
.collection::<Recipe>("recipes")
.delete_one(doc! {"_id": b_id.as_ref().unwrap()}, None)
.await
.is_err()
{
return status::Custom(
Status::BadRequest,
Json(
json!({"status": "error", "message":format!("Recipe ({}) could not be deleted", b_id.unwrap())}),
),
);
};
status::Custom(
Status::Accepted,
Json(
json!({"status": "", "message": format!("Recipe ({}) successfully deleted", b_id.unwrap())}),
),
)
}

This route deletes a single document by its _id value.

14

Start your application by running the following command in your terminal:

cargo run

In another terminal window, run the following command to test the create_recipe() route:

curl -v --header "Content-Type: application/json" --request POST --data '{"title":"simple bread recipe","ingredients":["water, flour"], "temperature": 250, "bake_time": 120}' http://127.0.0.1:8000/recipes
{"status":"success","message":"Recipe (684c4245f5a3ca09efa92593) created successfully"}

Run the following command to test the get_recipes() route:

curl -v --header "Content-Type: application/json" --header "Accept: application/json" http://127.0.0.1:8000/recipes/
[{"_id":...,"title":"artisan","ingredients":["salt","flour","water","yeast"],"temperature":404,"bake_time":5},
{"_id":...,"title":"rye","ingredients":["salt"],"temperature":481,"bake_time":28},...]

Run the following command to test the delete_recipe() route. Replace the <id> placeholder with a known _id value from your collection, which might resemble 68484d020f561e78c03c7800:

curl -v --header "Content-Type: application/json" --header "Accept: application/json" --request DELETE http://127.0.0.1:8000/recipes/<id>
{"status":"","message":"Recipe (68484d020f561e78c03c7800) successfully deleted"}

In this tutorial, you learned how to build a simple web application with Rocket to perform CRUD operations.

To learn more about CRUD operations, see the following guides:

Back

Store Large Files