DEV Community

Cover image for How to create a web app in Rust with Rocket and Diesel
Matt Angelosanto for LogRocket

Posted on • Originally published at blog.logrocket.com

How to create a web app in Rust with Rocket and Diesel

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
Enter fullscreen mode Exit fullscreen mode

Let’s dive right in!

Jump ahead:

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 
Enter fullscreen mode Exit fullscreen mode

When you run the command above, a blog directory will be automatically generated with the following structure:

.
├── Cargo.toml
└── src
    └── main.rs
Enter fullscreen mode Exit fullscreen mode

Next, we will need to add Diesel, Rocket, and other dependencies to the Cargo.toml file. These additional dependencies include:

#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"]
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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: Rust, Rocket, and Diesel Project Set Up 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
)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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: Example of the Diesel Migration

Here’s how the table will look in PostgreSQL: List of Tables in the Blog Database Table Description in Rust

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,
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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))
}
Enter fullscreen mode Exit fullscreen mode

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,
}
Enter fullscreen mode Exit fullscreen mode

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))
}
Enter fullscreen mode Exit fullscreen mode

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()})
}
Enter fullscreen mode Exit fullscreen mode

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");
Enter fullscreen mode Exit fullscreen mode

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"]
Enter fullscreen mode Exit fullscreen mode

Let’s take a closer look at the response:

Template::render("posts", context! {posts: &results, count: results.len()})
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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())
}
Enter fullscreen mode Exit fullscreen mode

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: Rust, Rocket, and Diesel Running 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: Blog Post Created With Rust, Rocket, and Diesel

When we check back again on the browser, we should see our new post. Created Web App With Rust, Rocket, and Diesel

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 Dashboard Free Trial Banner

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)