Written by Eze Sunday✏️
For seven years now, the Rust programming language has been voted the most loved programming language, according to a survey by Stack Overflow. Its popularity stems from its focus on safety, performance, built-in memory management, and concurrency features. All of these reasons make it an excellent choice for building web applications.
However, Rust is a system programming language. How do you use it to create web applications? Enter Rocket, Actix, Warp, and more. These web frameworks enable developers to create web applications with Rust.
Rocket and Diesel provide a powerful and efficient toolset for building web apps in Rust. In the rest of this article, we will go over how to create a web app using Rust, Rocket, and Diesel. We’ll go over setting up the development environment, examining the different components, setting up API endpoints, and rendering HTML.
To get ultimate value from this piece, you’ll need a basic understanding of Rust. You will also need to have Rust and PostgreSQL database installed and running. You can follow the documentation to install Rust for your operating system and download PostgreSQL for your OS from the official website. If you are using macOS, you can install and get your PostgreSQL database up and running quickly by running the following commands on your terminal:
brew update && brew install postgresql && brew services start postgresql
Let’s dive right in!
Jump ahead:
- Intro to Rocket and Diesel
- Setting up Rocket and Diesel
- Building the Rocket model
- Creating a post
- How to view posts
- Testing with Postman and the web browser
Intro to Rocket and Diesel
Rocket is a Rust web framework with built-in tools developers need to create efficient and secure web apps while maintaining flexibility, usability, memory, and type safety with a clean and simple syntax.
As is customary for most web frameworks, Rocket allows you to use object-relational mappers (ORMs) as a data access layer for your application. Rocket is ORM agnostic, which means you can use any Rust ORM of your choice to access your database in your Rocket application. In this article, we’ll use Diesel ORM as our ORM of choice as it’s one of the most popular Rust ORMs. At the time of writing, Diesel ORM supports PostgreSQL, MySQL, and SQLite databases.
Setting up Rocket and Diesel
First things first, let’s create a new Rust Binary-based application with Cargo, as shown below:
cargo new blog --bin
When you run the command above, a blog
directory will be automatically generated with the following structure:
.
├── Cargo.toml
└── src
└── main.rs
Next, we will need to add Diesel, Rocket, and other dependencies to the Cargo.toml
file. These additional dependencies include:
-
dotenvy
crate: To allow us to use environment variables from the.env
file -
serde
: For data serialization and deserialization -
dependencies.rocket_contrib
: To work withserde
for data deserialization -
dependencies.rocket_dyn_templates
: For dynamic templating engine support
#Cargo.toml
[package]
name = "blog"
version = "0.1.0"
edition = "2021"
[dependencies]
rocket = { version = "0.5.0-rc.2", features=["json"]}
diesel = { version = "2.0.0", features = ["postgres", "r2d2"] }
dotenvy = "0.15"
serde = "1.0.152"
[dependencies.rocket_dyn_templates]
features = ["handlebars"]
[dependencies.rocket_contrib]
version = "0.4.4"
default-features = false
features = ["json"]
Now, we have all the dependencies. Take note that I enabled json
in the features
to make serde json
available in the Rocket application. I also enabled postgres
and r2d2
for Diesel to make the PostgreSQL and connection pooling feature available.
Installing the Diesel CLI
Diesel provides a CLI that allows you to manage and automate the Diesel setup database reset and database migrations processes. Install the Diesel CLI by running the command below:
cargo install diesel_cli --no-default-features --features postgres
NOTE: Make sure you have PostgreSQL installed; otherwise, you’ll have errors.
Next, create a .env
file and add your database connection string as shown below:
DATABASE_URL=postgres://username:password@localhost/blog
Keep in mind that the database blog
must exist on your Postgres
database. From there, run the diesel setup
command on your Terminal.
This command will create a migration
file and a diesel.toml
file with the necessary configurations, as shown below:
├── Cargo.toml
├── diesel.toml
├── migrations
│ └── 00000000000000_diesel_initial_setup
│ ├── down.sql
│ └── up.sql
└── src
└── main.rs
Because this project is a blog with a posts table to store all posts, we need to create a migration with diesel migration generate posts
code.
Once you run that command, the response should look like this: Now, when you open the up.sql
file in the migration
directory, there should be no content in it. Next, add the SQL query to create the posts
table with the code below:
CREATE TABLE posts (
id SERIAL PRIMARY KEY,
title VARCHAR NOT NULL,
body TEXT NOT NULL,
published BOOLEAN NOT NULL DEFAULT FALSE
)
Also, open the down.sql
file and add a query to drop the table
:
-- This file should undo anything in `up.sql`
DROP TABLE posts
Running the migration
Once these files are updated, we can run the migration. You will need to create the down.sql
file and make sure it’s accurate so that you can quickly roll back your migration with a simple command. To apply the changes we just made to the migration
file, run the diesel migration run
command. You’ll notice that the schema.rs
file and the database table
will be created. Here’s what it should look like:
Here’s how the table will look in PostgreSQL:
To redo the migration
, run the diesel migration redo
command. The table we just created will be deleted from the Postgres database. Of course, this is not something you want to do when you have real data because you’ll delete everything. So far, we’ve set up Diesel. We need to set up the blog post model to allow Rocket to interact with the schema effectively.
Building the Rocket model
To build the Rocket model, create a Models
directory and a mod.rs
file. Then, add the following content to it:
// models/mod.rs
use super::schema::posts;
use diesel::{prelude::*};
use serde::{Serialize, Deserialize};
#[derive(Queryable, Insertable, Serialize, Deserialize)]
#[diesel(table_name = posts)]
pub struct Post {
pub id: i32,
pub title: String,
pub body: String,
pub published: bool,
}
Notice that we are deriving Queryable, Insertable, Serialize, Deserialize
. Queryable
will allow us to run select queries on the table. If you don’t want the table to be selectable, you can ignore it, the same as the Insertable
. Insertable
allows you to create a record in the database. And finally, Serialize
and Deserialize
automatically allow you to serialize and deserialize the table.
If you are following along, your application structure should look like this:
.
├── Cargo.lock
├── Cargo.toml
├── diesel.toml
├── target
├── migrations
│ ├── 00000000000000_diesel_initial_setup
│ │ ├── down.sql
│ │ └── up.sql
│ └── 2023-01-18-115141_posts
│ ├── down.sql
│ └── up.sql
└── src
├── main.rs
├── models
│ └── mod.rs
└── schema.rs
At this point, we have everything all setup! Now, let’s write a service to create a blog post via an API and display it in the browser — that way, we get to see how to handle both scenarios. Before making services to create and view blog posts, let’s connect to the database.
Connecting to the database
Create a services
directory and add the following content to the mod.rs
file:
extern crate diesel;
extern crate rocket;
use diesel::pg::PgConnection;
use diesel::prelude::*;
use dotenvy::dotenv;
use rocket::response::{status::Created, Debug};
use rocket::serde::{json::Json, Deserialize, Serialize};
use rocket::{get, post };
use crate::models;
use crate::schema;
use rocket_dyn_templates::{context, Template};
use std::env;
pub fn establish_connection_pg() -> PgConnection {
dotenv().ok();
let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
PgConnection::establish(&database_url)
.unwrap_or_else(|_| panic!("Error connecting to {}", database_url))
}
In the code above, we imported all the necessary crates
we’ll use in the service and created the Postgres connection
function. We’ll reuse this function when we create and query data from the database. Now that we have a database connection let’s implement a function to create a record in the database.
Creating a post
Creating a record with Diesel ORM is pretty straightforward. We’ll start by creating a struct that represents the structure of the data we are expecting from the client and enable Serialization
via the derive attribute as shown below:
//service/mod.rs
#[derive(Serialize, Deserialize)]
pub struct NewPost {
title: String,
body: String,
}
Next, we’ll create the actual function that receives the data from the client and processes it:
type Result<T, E = Debug<diesel::result::Error>> = std::result::Result<T, E>;
#[post("/post", format = "json", data = "<post>")]
pub fn create_post(post: Json<NewPost>) -> Result<Created<Json<NewPost>>> {
use self::schema::posts::dsl::*;
use models::Post;
let connection = &mut establish_connection_pg();
let new_post = Post {
id: 1,
title: post.title.to_string(),
body: post.body.to_string(),
published: true,
};
diesel::insert_into(self::schema::posts::dsl::posts)
.values(&new_post)
.execute(connection)
.expect("Error saving new post");
Ok(Created::new("/").body(post))
}
The create_post
function above accepts a post
object as a parameter and returns a Result that
could be an error
or a successful creation. The attribute #[post("/posts")]
indicates that it’s a POST
request. The Created
response returns a 200
status code, and this line Created::new("/").body(post)
returns both the 200
status code and the record
that was just inserted if the insertion was successful as a JSON Deserialized object
.
How to view posts
Now that we can create records, let’s create the functionality to view the records we’ve created in the browser. The creation logic was for a Rest API. Now we need to meddle with HTML templates:
#[get("/posts")]
pub fn list() -> Template {
use self::models::Post;
let connection = &mut establish_connection_pg();
let results = self::schema::posts::dsl::posts
.load::<Post>(connection)
.expect("Error loading posts");
Template::render("posts", context! {posts: &results, count: results.len()})
}
In the code above, we requested all the posts in the posts
table. Note that the #[get("/posts")]
attribute indicates that it’s a GET
request. We can also use a filter
only to fetch published posts. For example, the code below will fetch all posts that have been published:
let results = self::schema::posts::dsl::posts
.filter(published.eq(true))
.load::<Post>(connection)
.expect("Error loading posts");
Notice that the function returns a Template
, right? Remember the Cargo.toml
file that we added handlebars
to as the templating engine? That function will return a template
, and handlebars
will take care of the rest:
[dependencies.rocket_dyn_templates]
features = ["handlebars"]
Let’s take a closer look at the response:
Template::render("posts", context! {posts: &results, count: results.len()})
The first argument is the handlebar’s template filename. We haven’t created it yet, so let’s do that. First, create a directory named templates
and ad the file posts.html.hbs
. Make sure the HTML
is in the name. Otherwise, Rocket might not be able to recognize the file as a template.
Add the code below as the content of the file:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Blog Posts</title>
</head>
<body>
<section id="hello">
<h1>Posts</h1>
New Posts
<ul>
{{#each posts}}
<li>Title: {{ this.title }}</li>
<li>Body: {{ this.body }}</li>
{{else}}
<p> No posts yet</p>
{{/each}}
</ul>
</section>
</body>
</html>
We use the #each
loop in the template to loop through the posts and display the content individually. By now, your directory structure should look like this:
.
├── Cargo.lock
├── Cargo.toml
├── diesel.toml
├── migrations
│ ├── 00000000000000_diesel_initial_setup
│ │ ├── down.sql
│ │ └── up.sql
│ └── 2023-01-18-115141_posts
│ ├── down.sql
│ └── up.sql
├── src
│ ├── main.rs
│ ├── models
│ │ └── mod.rs
│ ├── schema.rs
│ └── services
│ └── mod.rs
└── templates
└── posts.html.hbs
Lastly, let’s add a route
and test the application. Open the main.rs
file and replace the existing "Hello, World!"
function with the following:
extern crate rocket;
use rocket::{launch, routes};
use rocket_dyn_templates::{ Template };
mod services;
pub mod models;
pub mod schema;
#[launch]
fn rocket() -> _ {
rocket::build()
.mount("/", routes![services::create_post])
.mount("/", routes![services::list])
.attach(Template::fairing())
}
In the code above, we imported the launch
macro that generates the main function — the application's entry point and returns Rocket<Build>
. You'll have to mount every route
you want to add. We use the attach
method to render the template and pass the fairing
trait to it.
That’s it. We are now ready to test the application. If you have followed through to this point, you are the real MVP.
Testing with Postman and the web browser
At this point, we’ve done the hard part. Let’s do the fun part, where you see how what we’ve built works. First, compile and run the application by running the cargo run
command on your terminal. You should see something like this if everything goes well: Go to http://127.0.0.1:8000/posts
via your browser (GET
request) to view all the posts you have created. Initially, there will be no posts. Let’s create one with Postman. So, we’ll make an HTTP POST
request to the /post
endpoint to create a blog post:
When we check back again on the browser, we should see our new post.
Conclusion
This article taught us how to create a web application with Rust, Rocket, and Diesel. We explored how to create an API endpoint, insert and read from a database, and how to render HTML templates. I hope you enjoyed reading it as much as I did writing it. For further reading, I encourage you to read this article about building a web app with Rocket.
You should also check out the GitHub repo for the built demo application. It should be a primary point of reference if you get confused here.
LogRocket: Full visibility into web frontends for Rust apps
Debugging Rust applications can be difficult, especially when users experience issues that are difficult to reproduce. If you’re interested in monitoring and tracking performance of your Rust apps, automatically surfacing errors, and tracking slow network requests and load time, try LogRocket.
LogRocket is like a DVR for web apps, recording literally everything that happens on your Rust app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app’s performance, reporting metrics like client CPU load, client memory usage, and more.
Modernize how you debug your Rust apps — start monitoring for free.
Top comments (0)