Definitive Guide
5 chapters
~25 min

Rocket: The Ultimate Guide (2023)

This is the most comprehensive tutorial on the Rocket framework online.In this Rocket tutorial, you will learn Rocket from scratch to an advanced level.You will learn how to build and deploy your first Rocket app.

SO
Solomon Eseme
Updated 12/17/2023
1
Chapter 1

Chapter 1: Complete Rocket Overview

1 articles
~3 min
1

Introduction to the Rocket Framework

This chapter introduced Rust and the Rocket framework, highlighting their advantages in backend web development.

3 min read

1.1 The Rise of Rust in Web Development

Rust, a system programming language, has steadily risen in prominence over recent years. It's known for its focus on safety, speed, and concurrency. Unlike other system languages, Rust guarantees memory safety by using a borrow checker to validate references, which prevents common pitfalls such as buffer overflows and dangling pointers.

Rust's performance and reliability make backend web development particularly appealing. It competes with C and C++ in speed but surpasses them in safety, making it an excellent choice for high-performance web servers and data-intensive applications.

1.2 Why Rust for Backend Development?

Safety: Rust's ownership model eliminates entire classes of bugs at compile-time, ensuring safer code without the overhead of a garbage collector.

Performance: Being a compiled language, Rust delivers extremely high performance. It's ideal for CPU-bound tasks and scenarios requiring low latency and high throughput.

Concurrency: Rust's approach to concurrency is proactive and robust, enabling developers to write efficient, thread-safe code without the inherent complexity and risks associated with parallel execution in other languages.

Modern Tooling: Rust has Cargo, a built-in package manager and build system, making dependency management and compilation straightforward.

Growing Ecosystem: Although younger than many other languages, Rust has a rapidly growing ecosystem, with an increasing number of libraries and frameworks suitable for various tasks, including web development.

Name

custom

Title

Rust Essentials

URL

https://masteringbackend.com/books/rust-essentials

description

This is the most comprehensive guide to the Rust Programming Language online. I’ll programmatically introduce you to Rust Programming Language in this Rust essential.

1.3 Introduction to the Rocket Framework

Rocket is a web framework for Rust that simplifies writing fast, secure web applications without sacrificing flexibility. It was created by Sergio Benitez and first released in 2016. Rocket is designed to be as ergonomic and flexible as possible, making it a great choice for beginners and experienced Rustaceans looking to develop web applications.

1.4 Key Features of Rocket

Type Safety: Rocket ensures type safety at compile-time, significantly reducing runtime errors.

Minimal Boilerplate: Rocket requires minimal boilerplate code, enabling rapid development and focusing more on business logic than boilerplate code.

Extensibility: Rocket is highly extensible, allowing developers to integrate third-party libraries or custom components easily.

Built-in Templating: Rocket supports several templating engines like Handlebars and Tera, allowing for dynamic content rendering in web applications.

Simple Configuration: Rocket's configuration system is straightforward, supporting environment-specific settings.

1.5 The Value Proposition of Rust and Rocket in Backend Development

Combining Rust and Rocket offers a compelling package for backend development. Rust's performance and reliability and Rocket's ease of use and expressiveness provide a potent toolkit for building web applications, from simple APIs to complex, data-intensive services.

Developers who choose Rust and Rocket can look forward to an enjoyable development experience that doesn't compromise on power or safety. This combination is particularly well-suited for applications where performance, security, and reliability are paramount.

1.6 Conclusion

This chapter introduced Rust and the Rocket framework, highlighting their advantages in backend web development. The following chapters will delve deeper into Rocket, exploring its capabilities and demonstrating how to leverage Rust's strengths to build robust, efficient web applications.

Stay tuned for Chapter Two, where we will closely examine the Rocket framework, its architecture, and how it simplifies building web applications in Rust.

2
Chapter 2

Chapter 2: Deep Dive into Rocket

1 articles
~6 min
1

A Deep Dive into the Rocket Framework

This chapter explored the Rocket framework, understanding its architecture, routing system, comparison with other frameworks, and its place in the Rust ecosystem. Rocket’s focus on simplicity, type safety, and flexibility makes it a standout choice for Rust-based web development.

6 min read

2.1 Understanding Rocket’s Core Philosophy

Rocket aims to make web development as simple and fast as possible without sacrificing power or control. It is designed around several key principles:

  • Ease of Use: Rocket's intuitive API and extensive documentation make it accessible to developers of all skill levels.

  • Type Safety: Leverages Rust's type system to catch errors at compile time, reducing runtime errors significantly.

  • Flexibility: While providing many built-in features, Rocket allows developers to plug in their tools or libraries as needed.

2.2 The Architecture of Rocket

Rocket follows a modular and extensible architecture:

  • Request Handling: At its core, Rocket is designed to handle HTTP requests efficiently. It uses a combination of listeners, routers, and responders to process requests and generate responses.

  • Fairings: These are Rocket’s middleware components that can be used to run code at different stages of the request-handling process.

  • State Management: Rocket provides a mechanism for managing the application state, allowing data to be shared safely across different application parts.

2.3 Routing in Rocket

You will be building HTTP web servers and APIs most of the time, so you should learn how Rocket treats and responds to HTTP requests. Rocket treats HTTP requests as a cycle, as explained below:

  • It determines the function responsible for handling the incoming request, a process known as routing.

  • It validates the incoming request to guard against anomalies. It validates the request against all functions until it is validated/satisfied.

  • It then processes the function being requested/validated, executing the body of the function.

  • It finally returns a response to the requesting client containing an HTTP status, headers, and body.

In the early 2000s, the Model-View-Controller method of building web applications was in vogue. Quite a lot has changed, but the fundamental idea remains the same. Building a web application requires a routing mechanism, a handler for handling the route, and a server to serve responses to requests made to that route.

With Rocket, you can specify that a function has a route by adding an attribute to the top of the function definition, like:

#[get("/")]
fn home_page() {}

You can then mount this function into Rocket to handle the function’s route. Rocket doesn’t require the main function, as a custom rocket function indirectly contains the main function. As such, you only need this:

#[get("/")]
fn home_page() {}

#[launch]
fn rocket() -> _ {}

To handle the route, you can use Rocket’s routes! functionality to handle it:

#[macro_use] extern crate rocket;

#[get("/")]
fn home_page() {}

#[launch]
fn rocket() -> _ {
	rocket::build().mount("/", routes![home_page])
}

Now, let’s put a dummy text inside of the home_page route function so we can see whether this works:

#[macro_use] extern crate rocket;

#[get("/")]
fn home_page() -> &'static str {
    "Hello, world!"
}

#[launch]
fn rocket() -> _ {
    rocket::build().mount("/", routes![home_page])
}

When you run cargo run for this, you get the output on your terminal:

❯ cargo run
    Blocking waiting for file lock on build directory
   Compiling rocket-web v0.1.0 (/Users/rustacean/rocket-web)
    Finished dev [unoptimized + debuginfo] target(s) in 14.26s
     Running `target/debug/rocket-web`
🔧 Configured for debug.
   >> address: 127.0.0.1
   >> port: 8000
   >> workers: 8
   >> max blocking threads: 512
   >> ident: Rocket
   >> IP header: X-Real-IP
   >> limits: bytes = 8KiB, data-form = 2MiB, file = 1MiB, form = 32KiB, json = 1MiB, msgpack = 1MiB, string = 8KiB
   >> temp dir: /var/folders/st/4pr4pf1d5hg5dwjnn8m336qw0000gn/T/
   >> http/2: true
   >> keep-alive: 5s
   >> tls: disabled
   >> shutdown: ctrlc = true, force = true, signals = [SIGTERM], grace = 2s, mercy = 3s
   >> log level: normal
   >> cli colors: true
📬 Routes:
   >> (home_page) GET /
📡 Fairings:
   >> Shield (liftoff, response, singleton)
🛡️ Shield:
   >> X-Content-Type-Options: nosniff
   >> Permissions-Policy: interest-cohort=()
   >> X-Frame-Options: SAMEORIGIN
🚀 Rocket has launched from <http://127.0.0.1:8000>

The logged output is a very helpful one that you can use to decipher what is happening and also get unstuck should there be a mistake from your end. Here's an explanation of the different parts of the logged message:

  1. "Blocking waiting for file lock on build directory": This message indicates that the application is waiting for a file lock on the build directory, which suggests that some other process may be concurrently accessing or modifying the build directory.

  2. "Compiling rocket-web v0.1.0 (/Users/rustacean/rocket-web)": This line shows that the Rust compiler is compiling a crate named "rocket-web" with version 0.1.0 located at the specified path.

  3. "Finished dev [unoptimized + debuginfo] target(s) in 14.26s": This line indicates that the compilation has finished, and it specifies the build configuration (development mode, unoptimized, and debuginfo) and the time it took to compile the target(s).

  4. "Running target/debug/rocket-web": This line shows that the compiled binary executable is being run. It specifies the path to the executable.

  5. "🔧 Configured for debug": This part of the message indicates that the application is configured for debugging.

  6. Configuration settings: The lines starting with ">>" list various configuration settings for the Rocket web application, such as the address, port, number of workers, maximum blocking threads, and other settings related to the web server.

  7. "📬 Routes": This section lists the defined routes in the web application. In this case, there is a single route named "home_page" defined for the HTTP GET request to the root ("/") URL.

  8. "📡 Fairings": Fairings are a concept in the Rocket web framework that applies middleware to requests and responses. In this case, there is a fairing named "Shield" with liftoff, response, and singleton modes.

  9. "🛡️ Shield": This section lists various security-related headers and settings applied by the "Shield" fairing, such as X-Content-Type-Options, Permissions-Policy, and X-Frame-Options.

  10. "🚀 Rocket has launched from [<http://127.0.0.1:8000>](<http://127.0.0.1:8000/>)": This line indicates that the Rocket web application has successfully launched, and it specifies the URL (http://127.0.0.1:8000) from which the application can be accessed.

In summary, this logged message provides information about the compilation and execution of a Rust web application using the Rocket framework. It includes details about the configuration settings, routes, and security settings applied to the application.

You can quickly Cmd + Click on the localhost URL to access your running route:

Untitled (43).png

Impressive. With this, we have routed our first endpoint using Rocket! Let’s learn how to create proper web applications with Rocket.

2.4 Conclusion

This chapter explored the Rocket framework, understanding its architecture, routing system, comparison with other frameworks, and its place in the Rust ecosystem. Rocket’s focus on simplicity, type safety, and flexibility makes it a standout choice for Rust-based web development.

In the next chapter, we will step into the practical aspects, demonstrating how to build a basic web application using Rocket. This will include setting up a project, understanding its structure, and building a simple CRUD application.

3
Chapter 3

Chapter 3: Building a Basic Web App with Rocket

1 articles
~3 min
1

Building a Basic Web Application with Rocket

This chapter covered the basics of setting up a Rocket project, understanding its structure, and building a simple CRUD application. We also explored how to manage shared state in a Rocket application. This foundational knowledge sets the stage for more advanced topics and real-world application development with Rocket.

3 min read

3.1 Setting Up a Rocket Project

Before building a web application with Rocket, you must set up the environment and project structure.

  • Installing Rust: Ensure Rust is installed on your system. You can do this using rustup, the recommended way to install the Rust programming language.

  • Creating a New Project: Use Cargo, Rust's package manager and build system, to create a new project: cargo new blog-engine --bin.

  • Adding Rocket Dependency: In your Cargo.toml file, add Rocket to your dependencies. Make sure to use the latest version compatible with your Rust version.

3.2 Understanding the Project Structure

A typical Rocket project has the following structure:

  • Cargo.toml: Configuration file for your Rust project, including dependencies.

  • src/main.rs: The entry point for your application. This is where you will define your Rocket instance and mount routes.

  • src/routes/: A directory for organizing your route handlers.

  • templates/: This directory will contain your HTML templates if you use templating engines.

3.3 Building a Simple CRUD Application

Let’s create a basic CRUD (Create, Read, Update, Delete) application to understand how Rocket handles web requests.

  1. Defining Your Models: For simplicity, let’s create a basic Todo struct in a new file, src/models.rs.

    pub struct Todo {
        pub id: usize,
        pub title: String,
        pub completed: bool,
    }
    
  2. Creating Route Handlers: In src/routes.rs, define functions for handling different HTTP methods corresponding to CRUD operations. Below, you will see how Rocket handles the creation of route handlers for functions:

    #[get("/")]
    fn read_all() -> ... { /* logic to display all todos */ }
    
    #[post("/", data = "<todo>")]
    fn create(todo: Json<Todo>) -> ... { /* logic to add a new todo */ }
    
    #[put("/<id>", data = "<todo>")]
    fn update(id: usize, todo: Json<Todo>) -> ... { /* logic to update a todo */ }
    
    #[delete("/<id>")]
    fn delete(id: usize) -> ... { /* logic to delete a todo */ }
    
  3. Setting Up the Rocket Instance: In src/main.rs, set up your Rocket instance and mount the routes.

    #[macro_use] extern crate rocket;
    
    mod models;
    mod routes;
    
    #[launch]
    fn rocket() -> _ {
        rocket::build().mount("/todos", routes![routes::read_all, routes::create, routes::update, routes::delete])
    }
    

3.4 Managing State in Rocket

Rocket provides a way to manage shared state across your application. This is especially useful for database connections or shared configuration.

  • Defining a Shared State: For example, if you want to share a list of todos, define a state in your main.rs.

    #[launch]
    fn rocket() -> _ {
        rocket::build()
            .manage(Mutex::new(Vec::<Todo>::new())) // Shared state
            .mount("/todos", routes![...])
    }
    
  • Accessing State in Routes: Use the State type in your route handlers to access the shared state.

    #[get("/")]
    fn read_all(state: &State<Mutex<Vec<Todo>>>) -> ... { /* access shared todos */ }
    

3.5 Running and Testing Your Application

  • Running the Application: Use cargo run to start your Rocket application.

  • Testing: Access your application via a browser or tools like curl to test the different routes.

3.6 Conclusion

This chapter covered the basics of setting up a Rocket project, understanding its structure, and building a simple CRUD application. We also explored how to manage shared state in a Rocket application. This foundational knowledge sets the stage for more advanced topics and real-world application development with Rocket.

4
Chapter 4

Chapter 4: Advanced Rocket Features and Database Integration

2 articles
~13 min
1

Advanced Rocket Features and Database Integration

In this chapter, we discussed the advanced features of Rocket, including its powerful routing capabilities, error-handling mechanisms, middleware integration, and database interaction. We also discussed how to build APIs and manage asynchronous code in Rocket. These concepts are pivotal for building scalable, efficient, and robust web applications with Rocket.

3 min read

4.1 Leveraging Rocket's Advanced Routing Techniques

Rocket's routing system goes beyond basic GET and POST methods. Here, we'll discuss some of its advanced capabilities:

  • Query Strings: Handling query strings in Rocket is straightforward. You can define query parameters as arguments in your route functions. For example:

    #[get("/items?<id>&<name>")]
    
  • Request Guards: These are a powerful feature of Rocket that allows for pre-processing requests. Request guards can be used for tasks like authentication and authorization. For example:

    #[get("/dashboard", rank = 1, guard = "UserLoggedIn")]
    

4.2 Error Handling and Custom Responses

Error handling is an essential aspect of web development. Rocket provides mechanisms for handling different types of errors:

  • Catchers: These are Rocket's way of handling HTTP errors. You can define custom catchers for different error statuses. For example:

    #[catch(404)]
    
  • Response Customization: Rocket allows for highly customizable responses, enabling you to return different types of content (JSON, HTML, XML, etc.) and status codes. For example:

    #[get("/hello", format = "text/plain")]
    

4.3 Integrating Middlewares with Fairings

Fairings in Rocket are equivalent to middleware in other web frameworks. They offer functionality to run custom code at various stages of the request-response cycle:

  • Request Fairings: These are executed before a request is processed. For example:

    .attach(AdHoc::on_request("Request Fairing", |req, _| Box::pin(async move {})))
    
  • Response Fairings: These are executed after a request has been processed, just before sending the response. For example:

    .attach(AdHoc::on_response("Response Fairing", |req, res| Box::pin(async move {})))
    

4.4 Managing Application State and Environment Configuration

  • State Management: Rocket's state management system is robust, allowing shared state between different parts of your application. For example:

    .manage(SharedState::new())
    
  • Environment Configuration: Rocket's configuration system supports different environments (development, staging, production), making it easy to manage environment-specific settings. For example:

    rocket::custom(config)
    

4.5 Database Integration in Rocket

Integrating a database is a critical part of most web applications. Rocket, combined with Rust's ORM libraries, provides a seamless database integration experience:

  • Setting Up a Database Connection: We'll explore how to set up and manage database connections using Rocket's state management. For example:

    .manage(db::init_pool())
    
  • Defining Models: You can define models that map directly to your database tables using ORMs like Diesel. For example:

    #[derive(Queryable)]
    
  • CRUD Operations: We'll look at performing CRUD operations on the database through Rocket's route handlers. For example:

    #[get("/items")]
    

4.6 Building APIs with Rocket

Rocket is particularly well-suited for building APIs:

  • JSON Support: Rocket has built-in support for JSON, making it easy to build JSON-based APIs. For example:

    #[get("/users", format = "application/json")]
    
  • API Versioning: We'll discuss strategies for versioning your API using Rocket. For example:

    #[get("/v1/items")]
    

4.7 Working with Asynchronous Code

Asynchronous programming is essential for performance in I/O-bound applications. Rocket supports Rust's async/await:

  • Async Routes: Learn how to define asynchronous route handlers in Rocket. For example:

    #[get("/async_route")]
    async fn async_route() {}
    
  • Integrating Async Libraries: Integrating asynchronous libraries and tasks into your Rocket application. For example:

    use tokio::task;
    

4.8 Best Practices for Advanced Rocket Applications

  • Error Handling: Implementing comprehensive error handling across your Rocket application. For example:

    #[catch(default)]
    
  • Security: Ensuring your Rocket application is secure against common web vulnerabilities. For example:

    #[launch]
    fn rocket() -> _ {
        rocket::build().attach(SecurityFairing)
    }
    
  • Performance Tuning: Tips for optimizing the performance of your Rocket application. For example:

    .attach(AdHoc::config::<ServerConfig>())
    

4.9 Conclusion

In this chapter, we discussed the advanced features of Rocket, including its powerful routing capabilities, error-handling mechanisms, middleware integration, and database interaction. We also discussed how to build APIs and manage asynchronous code in Rocket. These concepts are pivotal for building scalable, efficient, and robust web applications with Rocket.

The next chapter will embark on a practical journey to develop a real-world application using Rocket. This will consolidate the concepts learned so far and demonstrate how to apply them in a complex, feature-rich application.

2

Developing a Real-World Application with Rocket

In this chapter, we will apply the knowledge acquired from the previous chapters to develop a real-world web application using Rocket. We'll build a blog platform, integrating user authentication, database operations, and dynamic content rendering.

10 min read

In this chapter, we will apply the knowledge acquired from the previous chapters to develop a real-world web application using Rocket. We'll build a blog platform, integrating user authentication, database operations, and dynamic content rendering.

5.1 Project Overview

The project is a blog platform with the following features:

  • User Authentication: Users can sign up, log in, and log out.

  • Blog Post Management: Users can create, read, update, and delete blog posts.

  • Comment System: Users can comment on posts.

  • Search and Tagging Functionality: Posts can be searched and tagged for better organization.

5.2 Setting Up the Project

Start by creating a new Rocket project:

cargo new rocket_blog --bin
cd rocket_blog

Edit your Cargo.toml to include Rocket and other necessary dependencies like bcrypt for password hashing and Tera for templating.

5.3 Database Setup and Models

In this section, we focus on setting up the PostgreSQL database and defining the models for our Rocket blog platform. Instead of using an ORM like Diesel, we'll write our SQL queries directly and execute them from .sql files within Rust functions. This approach gives us more control over the database interactions.

Step 1: Setting Up the Database

  1. Install PostgreSQL: Ensure PostgreSQL is installed on your system.

  2. Create a New Database: Create a new database for the blog platform. You can use a PostgreSQL client or the command line:

    CREATE DATABASE rocket_blog;
    
  3. Database Configuration: In your Rocket project, configure the database connection. You can use environment variables or a configuration file to store your database credentials.

    In your Rocket.toml file:

    [global.databases]
    rocket_blog_db = { url = "postgres://username:password@localhost/rocket_blog" }
    

    Replace username and password with your actual PostgreSQL credentials.

Step 2: Creating SQL Files

Create a new directory in your project, sql, and add your SQL files here. For instance:

  • create_users_table.sql

  • create_posts_table.sql

  • create_comments_table.sql

Here's an example for create_users_table.sql:

CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    username VARCHAR(255) NOT NULL,
    password_hash VARCHAR(255) NOT NULL,
    email VARCHAR(255) UNIQUE NOT NULL,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);

Repeat this process for posts, comments, and other tables you need.

Step 3: Writing Rust Functions to Execute SQL Queries

In your Rust project, create a module to handle database operations. For example, src/db.rs.

  1. Database Connection Pool: Set up a connection pool to manage your database connections. You can use the tokio-postgres crate along with deadpool_postgres for asynchronous pool management.

    use deadpool_postgres::{Config, Manager, Pool};
    use tokio_postgres::NoTls;
    
    pub fn create_pool() -> Pool {
        let mut cfg = Config::new();
        cfg.host = Some("localhost".to_string());
        cfg.user = Some("username".to_string());
        cfg.password = Some("password".to_string());
        cfg.dbname = Some("rocket_blog".to_string());
        let manager = Manager::new(cfg, NoTls);
        Pool::new(manager, 16)
    }
    
  2. Executing SQL Files: Create functions to execute SQL queries from your .sql files. For example, a function to create the user's table:

    use std::fs;
    
    pub async fn create_users_table(pool: &Pool) -> Result<(), Box<dyn std::error::Error>> {
        let sql = fs::read_to_string("sql/create_users_table.sql")?;
        let conn = pool.get().await?;
    
        conn.execute(sql.as_str(), &[]).await?;
        Ok(())
    }
    
    pub async fn create_posts_table(pool: &Pool) -> Result<(), Box<dyn std::error::Error>> {
        let sql = fs::read_to_string("sql/create_posts_table.sql")?;
        let conn = pool.get().await?;
    
        conn.execute(sql.as_str(), &[]).await?;
        Ok(())
    }
    
    pub async fn create_comments_table(pool: &Pool) -> Result<(), Box<dyn std::error::Error>> {
        let sql = fs::read_to_string("sql/create_comments_table.sql")?;
        let conn = pool.get().await?;
    
        conn.execute(sql.as_str(), &[]).await?;
        Ok(())
    }
    
  3. Calling the Function: In your main application or during initialization, call this function to create the necessary tables.

    #[tokio::main]
    async fn main() {
        let pool = create_pool();
        create_users_table(&pool).await.expect("Failed to create users table.");
        create_posts_table(&pool).await.expect("Failed to create posts table.");
        create_comments_table(&pool).await.expect("Failed to create comments table.");
    
        // Initialize and launch Rocket application...
    }
    

Repeat this process for other tables and queries. This approach of using raw SQL provides you with fine-grained control over your database operations, allowing for complex queries and database interactions that may be cumbersome with an ORM.

5.4 Implementing User Authentication

In this step, we'll implement user authentication using direct PostgreSQL queries, including registration, login, and logout functionalities for our Rocket blog platform.

Step 1: Setting Up User Authentication

  1. Add Dependencies: Ensure your Cargo.toml includes necessary dependencies like bcrypt for hashing passwords and jsonwebtoken for handling JSON Web Tokens (JWT) if you use them for session management.

    [dependencies]
    bcrypt = "0.15.0"
    jsonwebtoken = "9.1.0"
    serde = "1.0.193"
    
  2. User Model: Update your user model in src/models.rs to include the necessary fields:

    use serde::{Serialize, Deserialize};
    
    #[derive(Serialize, Deserialize)]
    pub struct User {
        pub id: i32,
        pub username: String,
        pub email: String,
        pub password_hash: String,
    }
    
  3. Password Hashing: Create a utility function for hashing passwords using bcrypt.

    use bcrypt::{hash, DEFAULT_COST};
    
    pub fn hash_password(password: &str) -> Result<String, bcrypt::BcryptError> {
        hash(password, DEFAULT_COST)
    }
    

Step 2: Implementing Registration and Login Logic

  1. Registration Handler: Create a route to handle user registration. This will involve receiving user data, hashing the password, and storing the user in the database. This will be done inside the body of the main.rs for now.

    #[post("/register", data = "<user>")]
    async fn register(user: Json<User>, db_pool: State<DbPool>) -> Result<String, String> {
        let hashed_password = hash_password(&user.password).map_err(|e| e.to_string())?;
        let insert_query = "INSERT INTO users (username, email, password_hash) VALUES ($1, $2, $3)";
    
        // Execute the query with the provided user data
        // ...
    
        Ok("User registered successfully".to_string())
    }
    
  2. Login Handler: Create a route that checks the provided credentials against the database for logging in.

    #[post("/login", data = "<login>")]
    async fn login(login: Json<Login>, db_pool: State<DbPool>) -> Result<String, String> {
        let find_user_query = "SELECT * FROM users WHERE email = $1";
    
        // Execute the query and fetch user data
        // Verify password using bcrypt
        // ...
    
        Ok("User logged in successfully".to_string())
    }
    
  3. Session Management: Depending on your approach (cookies, JWT, etc.), implement session creation upon successful login.

Step 3: Implementing Logout

  1. Logout Handler: If using JWT, this might involve the client deleting the token. For cookie-based sessions, it would involve clearing the session cookie.

    #[post("/logout")]
    async fn logout() -> Result<&'static str, String> {
        // Invalidate session or token
        Ok("Logged out")
    }
    

Step 4: Integrating Authentication with the Database

  1. Database Functions: In your src/db.rs, add functions to insert a new user during registration and fetch a user during login.

    use tokio_postgres::{Client, Error};
    use crate::blogengine::models::User;
    
    pub async fn create_user(client: &Client, username: &str, email: &str, password_hash: &str) -> Result<(), Error> {
        let stmt = "INSERT INTO users (username, email, password_hash) VALUES ($1, $2, $3)";
        client.execute(stmt, &[&username, &email, &password_hash]).await?;
        Ok(())
    }
    
    pub async fn get_user_by_email(client: &Client, email: &str) -> Result<User, Error> {
        let stmt = "SELECT id, username, email, password_hash FROM users WHERE email = $1";
        let row = client.query_one(stmt, &[&email]).await?;
    
        Ok(User {
            id: row.get(0),
            username: row.get(1),
            email: row.get(2),
            password_hash: row.get(3),
        })
    }
    
  2. Using the Database Functions: In your route handlers (register and login), call these functions to interact with the database.

Now, let’s write the complete main.rs file:

pub mod blogengine;

#[macro_use] extern crate rocket;
use rocket::serde::json::{Json, json};
use rocket::State;
use tokio_postgres::NoTls;
use deadpool_postgres::{Config, Pool};
use crate::blogengine::db::{create_user, get_user_by_email};
use crate::blogengine::models::{User, hash_password};

// Define DbPool as a type alias for a connection pool
type DbPool = Pool;

// Initialize the database pool
fn init_db_pool() -> DbPool {
    let mut cfg = Config::new();
    cfg.host = Some("localhost".to_string());
    cfg.user = Some("username".to_string());
    cfg.password = Some("password".to_string());
    cfg.dbname = Some("rocket_blog".to_string());
    let pool = cfg.create_pool(NoTls).expect("Database pool creation failed");
    pool
}

#[post("/register", data = "<user_data>")]
async fn register(user_data: Json<User>, db_pool: State<DbPool>) -> Result<Json<&'static str>, String> {
    let conn = db_pool.get().await.map_err(|e| e.to_string())?;
    let hashed_password = hash_password(&user_data.password_hash).map_err(|e| e.to_string())?;
    create_user(&conn, &user_data.username, &user_data.email, &hashed_password).await.map_err(|e| e.to_string())?;
    Ok(json!("User registered successfully"))
}

#[post("/login", data = "<login_data>")]
async fn login(login_data: Json<User>, db_pool: State<DbPool>) -> Result<Json<&'static str>, String> {
    let conn = db_pool.get().await.map_err(|e| e.to_string())?;
    let user = get_user_by_email(&conn, &login_data.email).await.map_err(|e| e.to_string())?;

    // Here you would verify the password using bcrypt and manage the session
    // ...

    Ok(json!("User logged in successfully"))
}

#[post("/logout")]
async fn logout() -> Result<Json<&'static str>, String> {
    // Invalidate session or token
    Ok(json!("Logged out"))
}

#[launch]
fn rocket() -> _ {
    let db_pool = init_db_pool();
    rocket::build()
        .manage(db_pool)
        .mount("/", routes![register, login, logout])
}

Awesome! You can now recreate the Blog and Comments models and implementations similarly.

5.5 Building the Blog Post Functionality

To implement CRUD operations for blog posts, we first need to define the Post model and then create the necessary routes and handlers for creating, reading, updating, and deleting blog posts. These operations will interact with the PostgreSQL database.

Step 1: Define the Post Model

In your models.rs, define a Post struct to represent blog posts:

#[derive(Serialize, Deserialize)]
pub struct Post {
    pub id: i32,
    pub title: String,
    pub content: String,
    pub author_id: i32, // Assuming each post is linked to a user
    // Add additional fields as needed (e.g., timestamps)
}

Step 2: Database Functions for Post CRUD Operations

In db.rs, add functions for CRUD operations on blog posts:

use crate::blogengine::models::Post;
use tokio_postgres::Client;

pub async fn create_post(client: &Client, title: &str, content: &str, author_id: i32) -> Result<(), tokio_postgres::Error> {
    let stmt = "INSERT INTO posts (title, content, author_id) VALUES ($1, $2, $3)";
    client.execute(stmt, &[&title, &content, &author_id]).await?;
    Ok(())
}

pub async fn get_posts(client: &Client) -> Result<Vec<Post>, tokio_postgres::Error> {
    let stmt = "SELECT id, title, content, author_id FROM posts";
    let rows = client.query(stmt, &[]).await?;

    let posts = rows.into_iter().map(|row| Post {
        id: row.get(0),
        title: row.get(1),
        content: row.get(2),
        author_id: row.get(3),
    }).collect();

    Ok(posts)
}

// Add similar functions for updating and deleting posts

Step 3: Route Handlers for Post Operations

In your main application file, define route handlers for each CRUD operation:

#[post("/posts", data = "<post_data>")]
async fn create_post(post_data: Json<Post>, db_pool: State<DbPool>) -> Result<Json<&'static str>, String> {
    let conn = db_pool.get().await.map_err(|e| e.to_string())?;
    blogengine::db::create_post(&conn, &post_data.title, &post_data.content, post_data.author_id).await.map_err(|e| e.to_string())?;
    Ok(json!("Post created successfully"))
}

#[get("/posts")]
async fn read_posts(db_pool: State<DbPool>) -> Result<Json<Vec<Post>>, String> {
    let conn = db_pool.get().await.map_err(|e| e.to_string())?;
    let posts = blogengine::db::get_posts(&conn).await.map_err(|e| e.to_string())?;
    Ok(Json(posts))
}

// Implement similar handlers for updating and deleting posts

#[launch]
fn rocket() -> _ {
    let db_pool = init_db_pool();
    rocket::build()
        .manage(db_pool)
        .mount("/", routes![register, login, logout, create_post, read_posts])
}

Now, let’s work on commenting under blog posts.

5.6 Adding Comments

We'll create a Comment model to add a comment system to our blog platform. Database functions to handle comment operations and the necessary route handlers.

Step 1: Define the Comment Model

In models.rs, define a Comment struct to represent comments on blog posts:

#[derive(Serialize, Deserialize)]
pub struct Comment {
    pub id: i32,
    pub post_id: i32,
    pub author_id: i32,
    pub content: String,
    // You may also want to include timestamps
}

Step 2: Database Functions for Comment Operations

In db.rs, add functions to insert and retrieve comments:

use crate::blogengine::models::Comment;

pub async fn create_comment(client: &Client, post_id: i32, author_id: i32, content: &str) -> Result<(), tokio_postgres::Error> {
    let stmt = "INSERT INTO comments (post_id, author_id, content) VALUES ($1, $2, $3)";
    client.execute(stmt, &[&post_id, &author_id, &content]).await?;
    Ok(())
}

pub async fn get_comments_for_post(client: &Client, post_id: i32) -> Result<Vec<Comment>, tokio_postgres::Error> {
    let stmt = "SELECT id, post_id, author_id, content FROM comments WHERE post_id = $1";
    let rows = client.query(stmt, &[&post_id]).await?;

    let comments = rows.into_iter().map(|row| Comment {
        id: row.get(0),
        post_id: row.get(1),
        author_id: row.get(2),
        content: row.get(3),
    }).collect();

    Ok(comments)
}

Step 3: Route Handlers for Comment Operations

In your main application file, define route handlers for creating and reading comments:

#[post("/posts/<post_id>/comments", data = "<comment_data>")]
async fn create_comment(post_id: i32, comment_data: Json<Comment>, db_pool: State<DbPool>) -> Result<Json<&'static str>, String> {
    let conn = db_pool.get().await.map_err(|e| e.to_string())?;
    blogengine::db::create_comment(&conn, post_id, comment_data.author_id, &comment_data.content).await.map_err(|e| e.to_string())?;
    Ok(json!("Comment added successfully"))
}

#[get("/posts/<post_id>/comments")]
async fn read_comments(post_id: i32, db_pool: State<DbPool>) -> Result<Json<Vec<Comment>>, String> {
    let conn = db_pool.get().await.map_err(|e| e.to_string())?;
    let comments = blogengine::db::get_comments_for_post(&conn, post_id).await.map_err(|e| e.to_string())?;
    Ok(Json(comments))
}

#[launch]
fn rocket() -> _ {
    let db_pool = init_db_pool();
    rocket::build()
        .manage(db_pool)
        .mount("/", routes![register, login, logout, create_post, read_posts, create_comment, read_comments])
}

Awesome! We are done with the MVP for our blog engine. We can now create a database, complete its configuration, and test the application.

5.7 Conclusion

This chapter provided a step-by-step guide to building a real-world web application using Rocket. Through this project, we demonstrated the application of concepts like database integration, user authentication, templating, and CRUD operations in a practical scenario. This project is a blueprint for building complex, feature-rich applications with Rocket.

5
Chapter 5

Conclusion: Rocket

0 articles
~0 min

In conclusion, this book explored the world of backend web development with Rust's Rocket framework, emphasizing the modern features that make it an excellent choice for this domain. Rocket's flexibility was highlighted, offering a highly customizable approach and allowing developers to tailor their web applications with preferred components like databases, mailing systems, ORMs, and logging support.

In summary, Rust's Rocket framework offers a potent solution for backend web development, combining safety, performance, and flexibility. Developers aspiring to create secure and high-performance web applications can embark on their journey to master web development with Rust and Rocket by following the steps and examples in this book.

Congratulations!

You've completed the "Rocket: The Ultimate Guide (2023)" definitive guide

Enjoyed this article?

Subscribe to our newsletter for more backend engineering insights and tutorials.