It’s 1:00 AM (🎵 soft jazz music). You’re lying in bed trying to get some sleep. The city lights shine into your bedroom, softly illuminating the wall in front of you. You can hear the rain outside and the sound of people going about their late-night business. You can’t fall asleep. Something is bothering you, and you feel like you have to get it off your chest. You have to tell someone, but you don’t want to do it on social media. You want to spill your guts but remain completely anonymous. If only there was a place you could do this…
This is the setting we are going to use for the project of this post — Late Night Confessions: A 🦀 Rust-based website built using Rocket (web framework), Diesel (ORM), and Askama (template rendering engine).
Overview
The site functionality is pretty simple:
- Show the user a random confession.
- Allow the user to add a new confession.
Since we want to keep our confessions anonymous, we will not create a signup or login form.
Our site has three types of requests to handle:
- Static content requests — to get static files (i.e., index.html, styles.css)
- API request to get a confession.
- API request to post a confession.
All confessions will be saved in a Postgres database instance. We will use Diesel to manage the database activities.
Our finished project will look something like this:
The full source code can be found on Github.
Launching a 🚀
We begin by creating a new crate for our project called late-night-rocket:
cargo new late-night-rocket
Next, let’s add Rocket and Failure as dependencies to our Cargo.toml:
[dependencies]
rocket = { git = "https://github.com/SergioBenitez/Rocket", version = "0.5.0-dev" }
failure = {version = "0.1.8", features = ["derive"] }
🔭 Why do we need to specifically specify the 0.5.0-dev version? Since we want our app to run on a stable Rust release, 0.5.0-dev is (as of writing this post) the only version that supports stable Rust.
Rocket makes heavy use of macros. This means our first order of business is to import all of them to our crate. We do this by adding the macrouse attribute to the top of our _main.rs file, pointing to the Rocket crate:
#[macro_use]
extern crate rocket;
Next, let’s add some use statements to import modules we would need:
use rocket::request::Form;
use rocket::response::status::NotFound;
use rocket::response::NamedFile;
use rocket::Rocket;
use std::path::PathBuf;
Finally, it’s time to create some routes!
- We begin with the root route responsible for rendering the content of index.html when the / path is called:
#[get("/")]
async fn root() -> Result<NamedFile, NotFound<String>> {
NamedFile::open("site/static/index.html")
.await
.map_err(|e| NotFound(e.to_string()))
}
🔬 So what do we have here?
- Line 1: Say hello to our first Rocket attribute — route. Using this attribute, we define an HTTP method for our route (i.e get, post), a path (either static or dynamic), and an optional data parameter (not needed here since we are defining a GET route).
- Lines 3–5: We use the NamedFile struct to try and open theindex.html file (we will create it next), and if we are successful, return the content of the file (the file extension determines the content type automatically). If we fail, we map the errors to a NotFound error type and return it. Notice the use of await on line 4. We are asynchronously loading the file so that other operations won't have to wait for the file to load to continue processing.
2. Next, static_files, as the name suggests, will handle requests for static files, such as images:
#[get("/<path..>")]
async fn static_files(path: PathBuf) -> Result<NamedFile, NotFound<String>> {
let path = PathBuf::from("site").join(path);
NamedFile::open(path).await.map_err(|e| NotFound(e.to_string()))
}
The route looks very similar to the root route, with one noticeable difference; unlike the staticroot route, this route is dynamic, meaning it matches anything after the / as the path variable. We then use this path variable and append it to the site folder (line 3) before trying to get the file (using NamedFile, just like in the previous route).
3. Next, we mount the new routes to a Rocket instance using the mount method:
fn rocket() -> Rocket {
rocket::ignite().mount("/", routes![root, static_files])
}
🔬So what do we have here?
We ignite (pun intended) a new Rocket instance, then we mount the routes using the mount method, which takes two parameters as input:
- A base path to use on all the associated routes (/ in our case)
- A list of routes via the routes! macro.
4. Finally, we need to launch our rocket for it to serve requests. There are two mechanisms to launch a Rocket instance, but we will focus on the preferred approach for this tutorial's purposes: the launch attribute. Add it above the rocket function defined in the previous step:
#[launch]
fn rocket() -> Rocket {
rocket::ignite().mount("/", routes![root, static_files])
}
5. At the root of our project, create a new folder named site and inside of it, create a folder named static.
6. Inside the static folder, create anindex.html file with the following content:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Late Night Confessions</title>
<link href="https://fonts.googleapis.com/css2?family=Lato:ital,wght@0,300;1,400&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/cirrus-ui@0.6.0/dist/cirrus.min.css" />
<link rel="stylesheet" href="/static/styles.css" />
</head>
<body>
<h1>Fly 🚀 me to the moon 🌙</h1>
</body>
</html>
7. Create an empty styles.css file in the same folder. We will populate it later on.
Go ahead and cargo run the project and then browse to localhost:8000. Congratulations, you have a working Rocket server!
Look ma! I can serve a page! 👶
With our server up and running, we are ready to take on the next task and pour some Diesel (pun intended) into our Rocket to gain database support!