Overview
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.
Prerequisites
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.
Steps
Select the asynchronous runtime.
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.
Insert sample data.
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.
Install Rocket.
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.
Configure Rocket.
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.
Learn about the app structure.
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.
Set up the database connection.
Paste the following code into the db.rs file:
use rocket_db_pools::{mongodb::Client, Database}; 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:
mod db; mod models; mod routes; use rocket::{launch, routes}; use rocket_db_pools::Database; 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.
Create data models.
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:
use mongodb::bson::oid::ObjectId; use rocket::serde::{Deserialize, Serialize}; pub struct Recipe { pub id: Option<ObjectId>, pub title: String, pub ingredients: Vec<String>, pub temperature: u32, pub bake_time: u32, }
Set up API routes.
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:
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:
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, ], )
Implement error handling and responses.
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.
Write CRUD operation routes.
Create
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:
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"})), ) }
Read
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:
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![]); }
Retrieve by ID
Paste the following get_recipe() route under the
// Paste get_recipe route below comment:
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.
Update
Paste the following update_recipe() route under the
// Paste update_recipe route below comment:
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.
Delete
Paste the following delete_recipe() route after the
// Paste delete_recipe route below comment:
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.
Test routes to perform CRUD operations.
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"}
Conclusion
In this tutorial, you learned how to build a simple web application with Rocket to perform CRUD operations.
Resources
To learn more about CRUD operations, see the following guides: