Building a Rust app with Perseus
Tags: rust, perseus, webdev
Published: 2023-07-05 17:55:51

**Written by [Eze Sunday](https://blog.logrocket.com/author/ezesunday/)✏️**

Rust is a popular system programming language, known for its robust memory safety features and exceptional performance. While Rust was originally a system programming language, its application has evolved. Now you can see Rust in different app platforms, mobile apps, and of course, in web apps — both in the frontend and backend, with frameworks like [Rocket](http://rocket.rs), [Axum](https://github.com/tokio-rs/axum), and [Actix](http://actix.rs) making it even easier to build web applications with Rust. 

Reactive web frameworks with server-side rendering (SSR), mostly JavaScript frameworks like Next.js, Nuxt.js, and Svelte, are gaining increasing popularity. Interestingly, developers from the Rust community are also building alternatives that leverage the safety and performance of both Rust and WebAssembly combined to build reactive frontends. Perseus is the newest Rust framework for building frontend components. 

[Perseus](https://framesurge.sh/perseus/) is a fast frontend web development framework for Rust with built-in support for reactivity using Sycamore, server-side rendering, and much more. [Sycamore](https://sycamore-rs.netlify.app/) is a frontend library that allows you to build interactive user interfaces with Rust. I’d say that Perseus is to Sycamore as Next.js is to React, so it’ll be helpful for you to have a fair understanding of Sycamore before jumping into using Perseus — although it’s not necessary to follow along in this article. 

In this article, we’ll learn how to build a Rust web application with Perseus and how to deploy it to your server. The project we’ll build is a to-do application that allows the user to add and delete tasks. Here is what the finished project will look like: ![Reactive To-do App Demo](http://blog.logrocket.com/wp-content/uploads/2023/06/reactive-todo-app-demo.gif) _Jump ahead_:

*   [Setting up our Perseus environment](#setting-perseus-environment)
*   [Exploring our Perseus app structure](#exploring-perseus-app-structure)
*   [App templates with Perseus](#app-templates-perseus)
*   [Defining Perseus state](#defining-perseus-state)
*   [Creating a to-do form](#creating-todo-form)
*   [Editing our to-do list functionality](#editing-todo-list-functionality)
*   [Adding a list view to our to-do app](#adding-list-view-todo-app)
*   [Deploying your Perseus app](#deploying-perseus-app)

## Prerequisites

To follow along with this tutorial, you should:

*   Have a basic understanding of Rust
*   Have Rust installed on your computer. In case you don’t have Rust installed, please see the [Rust installation guide](https://doc.rust-lang.org/book/ch01-01-installation.html)

## Setting up our Perseus environment <a name="setting-perseus-environment">

Before you can start building your Rust application with Perseus, you'll need to set up your environment. This involves installing the Perseus command-line interface (CLI) and initializing your Perseus app. Let's walk through the installation process step by step.

### Installing the Perseus CLI

To begin, install the Perseus CLI, which allows you to manage your Perseus application seamlessly. Open your terminal and run the following command:

```bash
cargo install perseus-cli
```

This command will download and install the Perseus CLI on your system and make it available globally. To confirm that your installation went well, run `perseus --version`; the response should be `perseus-cli 0.4.2`.

### Initializing your Perseus app

Once the installation is complete, navigate to the directory where you want your Perseus application to reside. From there, initialize your app by running the following code:

```bash
perseus init my-todo-app
```

This will scaffold a Perseus boilerplate code that will serve as a starting point for your application. Think of it as the equivalent of `create-react-app` in React.js. The initialization process might take some time, depending on your network strength, and, once it's done, you can proceed to preview your setup by running:

```rust
perseus serve -w
```

Notice the `-w` flag. It’s optional. While `perseus serve` runs your code, the `-w` flag watches your code changes and updates the UI accordingly. When the app build is complete, navigate to http://localhost:8080 to see the demo welcome to Perseus app: ![Perseus Homepage](http://blog.logrocket.com/wp-content/uploads/2023/06/perseus-homepage.png) 

At this point, if you open the `Cargo.toml` file, you’ll see everything that’s been set up for you to ensure that Perseus works as expected. It should look like this:

```toml
# Cargo.toml
[package]
name = "simple-todo-app"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

# Dependencies for the engine and the browser go here
[dependencies]
perseus = { version = "=0.4.2", features = [ "hydrate" ] }
sycamore = "^0.8.1"
serde = { version = "1", features = [ "derive" ] }
serde_json = "1"

# Engine-only dependencies go here
[target.'cfg(engine)'.dependencies]
tokio = { version = "1", features = [ "macros", "rt", "rt-multi-thread" ] }
perseus-axum = { version = "=0.4.2", features = [ "dflt-server" ] }

# Browser-only dependencies go here
[target.'cfg(client)'.dependencies]
```

From the `Cargo.toml` file above, we can see that the Perseus version at the time of publication is `0.4.2` and has the following dependencies that are common to both the engine side (server-side) and client side of a Perseus application: `sycamore`, [`serde`](https://serde.rs/), and [`serde_json`](https://crates.io/crates/serde_json). 

Perseus applications also make use of [`tokio`](https://github.com/tokio-rs/axum), a Rust asynchronous runtime, and `perseus-axum` — an integration that streamlines the use of the Perseus frontend framework with the Axum web framework, specifically on the engine side. The configuration file also shows a section for browser-only dependencies. Even though there are no default dependencies in this section, it indicates a place where such dependencies could be added as your application evolves. 

Now that the app setup is complete, let’s tour the app structure we just created and start building.

## Exploring our Perseus app structure <a name="exploring-perseus-app-structure">

Our app structure will look like this at the beginning:

```rust
├── Cargo.lock
├── Cargo.toml
└── src
    ├── main.rs
    └── templates
        ├── index.rs
        └── mod.rs
```

`src/main.rs` is the natural main entry point of the app and it contains a single function, the main function that returns the PerseusApp:

```rust
mod templates;
use perseus::prelude::*;

#[perseus::main(perseus_axum::dflt_server)]
pub fn main<G: Html>() -> PerseusApp<G> {
    PerseusApp::new()
        .template(crate::templates::index::get_template())
}
```

Notice that the main function has an attribute: `[perseus::main(perseus_axum::dflt_server)]`. Attributes are metadata applied to modules, crates, structs, or functions to enhance their functionality. 

In the case of the Perseus main function attribute, the attribute essentially makes the main function the entry point of the Perseus app. The attribute takes `perseus_axum::dflt_server` as an argument, which sets up an Axum web server for your application and yes, Perseus uses the Axum server by default to set up the Perseus engine. 

Additionally, the main function returns a PerseusApp, which is like the root node for all the templates in your Perseus application. Now, let’s create some views.

## App templates with Perseus <a name="app-templates-perseus">

In a Perseus application, the template directory is essentially where all your app views are located by default. Each file in the template directory represents a page and returns a Perseus template. 

For instance, let's consider the following code:

```rust
pub fn get_template<G: Html>() -> Template<G> {
    Template::build("index").view(index_page).head(head).build()
}
```

Here, we're creating a template for the homepage (or `index`) of our web app. And then we go ahead and assign the `index_page` (a view function) as the view we want to display. We’ll then pass the template to the Perseus app in the `main` function as we’ve seen earlier, like so:

```rust
mod templates;
PerseusApp::new().template(crate::templates::index::get_template())
```

For most of it, in this tutorial, we’ll write our code in the `index.rs` template file. Let’s get into defining the state of the application.

## Defining Perseus state

In a Perseus application, a "state" refers to the data that a view or template depends on to render correctly. The state comprises different data types defined in a struct. And it’s crucial to the reactivity of the app as the application updates each time there is a change in the state. 

Let’s define a `TodoListState`. This state will serve as a data store for all the todos we’ll create:

```rust
use perseus::prelude::*;
use serde::{Deserialize, Serialize};
use std::rc::Rc;
use sycamore::prelude::*;
use web_sys::Event;

#[derive(Serialize, Deserialize, ReactiveState, Clone)]
#[rx(alias = "TodoListStateRx")]
struct TodoListState {
    todos: Vec<String>,
    new_todo: String,
}
```

There is a lot of abstraction in this code, so let’s break it down. 

In this `TodoListState` struct, we have two parts: `todos` and `new_todo`. The `todos` part is a list of your tasks — a vector of strings. The `new_todo` is also a string, and it's used to hold the new task that you want to add to the vector of strings (todos). 

Now, let's talk about those attributes applied to the `struct`:

*   `Serialize` and `Deserialize`: These mean that your `TodoListState` can be turned/serialized into a format like JSON and also be converted back into the structure that your program can understand (that's `Deserialize`). Remember the Serde library we added to the dependency list initially? This is exactly where we use it
*   `ReactiveState`: This is a custom derive attribute specific to Perseus. It tells the program that if anything in `TodoListState` changes, like adding a new task, Perseus should automatically update the parts of the app that display this information
*   `Clone`: The `Clone` trait in Rust allows for the duplication of an object. This simply means that the `TodoListState` can be duplicated, or cloned. So, you can create an exact copy of your task list if you want to
*   `#[rx(alias = "TodoListStateRx")]`: This is a custom attribute related to the reactive system of Perseus, which is built on Sycamore. This attribute allows you to provide an alias for the reactive version of the state. In this case, the reactive version of `TodoListState` can be referred to as `TodoListStateRx`. We’ll assign it to functions that contain reactive state code

Also, on the engine side, we initialize the state with some data as shown below:

```rust
#[engine_only_fn]
async fn get_build_state(_info: StateGeneratorInfo<()>) -> TodoListState {
    TodoListState {
        todos: vec!["Grocery shopping".to_string(), "Exercise for 30 minutes every morning".to_string()],
        new_todo: "".to_string(),
    }
}
```

Now, let’s add a heading and the form that we’ll use to add a new to-do to the list of to-dos:

```rust
fn header<G: Html>(cx: Scope) -> View<G> {
    view! { cx,
        h1 { "A simple Reactive Todo App" }
    }
}

#[auto_scope]
fn todo_form<G: Html>(cx: Scope, state: &TodoListStateRx) -> View<G> {
    let new_todo_form = new_todo(cx.clone(), state);
    view! { cx,
        div {
            (new_todo_form)
        }
    }
}
```

Let’s demystify each of the functions in the code above; we’ll start with the `header` function:

```rust
fn header<G: Html>(cx: Scope) -> View<G> {
    view! { cx,
        h1 { "A simple Reactive Todo App" }
    }
}
```

The `header` view function takes a scope, shorthand as `cx`; this convention is borrowed from Sycamore and is a requirement for every view function we pass to Sycamore. For simplicity, `Scope` is used to track any reactive variables or computations within a view, so it can automatically update the view whenever those values change.

## Creating a to-do form <a name="creating-todo-form">

In the code above, we leverage the `view!` macro to return the view that will be rendered. In the view, we pass an `h1` element with its contents in the curly braces. This is a pretty simple view function, so let’s take a look at the `todo_form` view function, which is a bit more complex:

```rust
#[auto_scope]
fn todo_form<G: Html>(cx: Scope, state: &TodoListStateRx) -> View<G> {
    let new_todo_form = new_todo(cx.clone(), state);
    view! { cx,
        div {
            (new_todo_form)
        }
    }
}
```

Notice that beyond the `Scope` argument, the `todo_form` view accepts a state as the second argument. Now, that’s a little complicated and Perseus needs to do a lot more work on this new view in the background. Therefore, we use the `#[auto_scope]` to abstract and simplify it. 

Essentially, if not for the `#[auto_scope]` attribute, the `todo_form` function argument would look like this:

```rust
fn todo_form<'page, G: Html>(cx: BoundedScope<'_, 'page>, state: &'page &TodoListStateRx) -> View<G> 
```

Thankfully, we don’t have to write that anymore, thanks to `#[auto_scope]`. 

So, in the `todo_form` view, we are calling the `new_todo` view and passing the state to it. It then does its magic and displays it in the form inside a div element as shown below:

```rust
let new_todo_form = new_todo(cx.clone(), state);
view! { cx,
    div {
        (new_todo_form) 
    }
}
```

### Adding a new task to the to-do list

Now that we have a form to create a to-do, let’s add the to-do to the list of to-dos in the state. First, let’s create the view that adds an item to the list of to-dos. Add the piece of code below in your `src/templates/index.rs` file:

```rust
#[auto_scope]
fn new_todo<G: Html>(cx: Scope, state: &TodoListStateRx) -> View<G> {
    view! {cx,
        form(on:submit = move |e: Event| {
            e.prevent_default();
            let new_todo: Rc<String> = state.new_todo.get().clone();

            if !new_todo.is_empty() {
                let new_todo_str: String = (*new_todo).clone();
                let mut todos = state.todos.get().as_ref().clone();
                todos.push(new_todo_str);
                state.todos.set(todos.to_vec());
                state.new_todo.set(String::new());
            }
        }) {
            input(id = "todo-input", type = "text", bind:value = state.new_todo)
            button(id = "todo-button") { "Add Item" }
        }
    }
}
```

In the above code, the `new_todo` function creates a form view for a new to-do item where the user can input a new to-do. When the form is submitted, it adds the new to-do to the list and clears the input field. 

Let’s take a quick overview of some key parts of the function:

1.  `let new_todo_str: String = (*new_todo).clone();`: This part of the code clones the new to-do the user just typed so it can be added to the to-do list
2.  `let mut todo: Vec<String> = state.todos.get().as_ref().clone();`: This part also retrieves the current to-do list from the state. But what is actually going on here?`state.todos.get()` is of type `Rc<T>`, reference counting; this type allows multiple owners of the same data, which is great for shared data in memory, but it doesn't allow you to directly clone the inner data. That’s why we introduce the `.as_ref()` method to get a reference to the value inside the `Rc<T>`, which is a type of `Vec<String>`. Then, we clone it with the `.clone()` method
3.  `todos.push(new_todo_str);`: Here, we add the new to-do to the to-do list
4.  `state.todos.set(todos.to_vec());`: This updates the to-do list in the state. Essentially, we replaced the entire existing todos in the state with the updated todos
5.  `state.new_todo.set(String::new());`: Finally, we reset the new to-do string in the state

## Editing our to-do list functionality <a name="editing-todo-list-functionality">

As we described in the previous section, we want to add the new to-dos to the list, display them, and also give the user a chance to delete the to-do. Here is the function for it:

```rust
#[auto_scope]
fn todo_list<G: Html>(cx: Scope, state: &TodoListStateRx) -> View<G> {
    view! { cx,
        div(id = "todo-items") {
            ul {
                (View::new_fragment(
                    state.todos.get()
                        .iter()
                        .rev()
                        .enumerate()
                        .map(|(index, item)| {
                            let item = item.clone();
                            view! { cx,
                                li(class = "todo-item") {
                                    (item)
                                    button( class = "remove-button", on:click = move |_| {
                                        let mut todos = state.todos.get().as_ref().clone();
                                        todos.retain(|todo| *todo != item);
                                        state.todos.set(todos.to_vec());
                                    }) { "x" }
                                }
                            }
                        })
                        .collect(),
                ))
            }
        }
    }
}
```

Basically, this view function takes in the state of the to-do list, and it generates a dynamic, interactive HTML list of to-do items. Each item comes with a button that allows for the removal of the item from the list. 

Let’s examine the code:

1.  `(View::new_fragment(`: This creates a new [DocumentFragment](https://developer.mozilla.org/en-US/docs/Web/API/DocumentFragment), which is a set of HTML elements without a parent element. It’s used as a temporary container to wrap and manipulate DOM nodes. It's used here to create a list of `li` elements, one for each to-do item
2.  `state.todos.get().iter().rev().enumerate().map(|(index, item)| {`: This line is iterating over the to-dos in the state, in reverse order so the most recent to-do appears first, and for each to-do item, it creates a new `li` element
3.  `button( class = "remove-button", on:click = move |_| {`: This creates a button for each to-do item, with an attached click event handler. When this button is clicked, it will trigger the code that removes the item from the list and resets the to-do list in this part of the code:

    ```rust
let mut todos = state.todos.get().asref().clone();
    todos.retain(|todo| *todo != item); // remove the todo
    state.todos.set(todos.tovec());```

## Adding a list view to our to-do app <a name="adding-list-view-todo-app">

Now, we can now take all the views we’ve created and put them together in a `todo_list_view` view function:

```rust
#[auto_scope]
fn todo_list_view<G: Html>(cx: Scope, state: &TodoListStateRx) -> View<G> {
    view! { cx,
        div(id = "todo-container") {
            (header(cx.clone()))
            (todo_form(cx.clone(), state))
            (todo_list(cx, state))
        }
    }
}
```

Finally, we can add the `todo_list_view` to the main template that we’ll pass to the Perseus app. Here is how the code will look:

```rust
#[engine_only_fn]
fn head(cx: Scope) -> View<SsrNode> {
    view! { cx,
        title { "Test App" }
        link(rel="stylesheet", href=".perseus/static/styles.css") {}
    }
}
pub fn get_template<G: Html>() -> Template<G> {
    Template::build("index")
        .build_state_fn(get_build_state)
        .view_with_state(todo_list_view)
        .head(head)
        .build()
}
```

Notice the `head` function? That’s like your regular HTML `<head>`. That’s where you define everything you want to be in the head. In our case, I am only linking the CSS file to it. 

Make sure you put the CSS file in the static directory at the root of your application, not in `.perseus/static/styles.css` but in `static/style.css`. Perseus will do the rest. Note that the static directory might not be there at first; you’ll have to create it. For this project, you can get all the source code, including the CSS, [from GitHub](https://github.com/ezesundayeze/perseus-todo-app). 

If you got to this point, congratulations! 🙏 

Go ahead and run the app by running `perseus serve--w` to ensure that our implementation works as expected at this point. Finally, let’s proceed to prepare the app for deployment.

## Deploying your Perseus app <a name="deploying-perseus-app">

Deploying a Perseus app is easy and straightforward. You just need to run the command `perseus deploy`, and wait for a couple of minutes. After the build process is complete, you can now deploy the build you’ll find in the `/pkg` directory to any server. 

But wait — Perseus doesn’t allow you to deploy the default error pages to production. You must create your own custom error views. So, create an additional view in the template directory `src/error.rs` to handle all the possible errors that could happen. You can customize it as you want and add this error view template to it:

```rust
use perseus::errors::ClientError;
use perseus::prelude::*;
use sycamore::prelude::*;

pub fn get_error_views<G: Html>() -> ErrorViews<G> {
    ErrorViews::new(|cx, err, _err_info, _err_pos| {
        match err {
            ClientError::ServerError { status, message: _ } => match status {
                404 => (
                    view! { cx,
                        title { "Page not found" }
                    },
                    view! { cx,
                        p { "Page now found" }
                    },
                ),
                // 4xx is a client error
                _ if (400..500).contains(&status) => (
                    view! { cx,
                        title { "Error" }
                    },
                    view! { cx,
                        p { "bad request" }
                    },
                ),
                // 5xx is a server error
                _ => (
                    view! { cx,
                        title { "Error" }
                    },
                    view! { cx,
                        p { "Sorry, internal server error." }
                    },
                ),
            },
            ClientError::Panic(_) => (
                view! { cx,
                    title { "Critical error" }
                },
                view! { cx,
                    p { "Sorry, something went wrong" }
                },
            ),
            ClientError::FetchError(_) => (
                view! { cx,
                    title { "Error" }
                },
                view! { cx,
                    p { "Network error, ensur you have stable internet connection)" }
                },
            ),
            _ => (
                view! { cx,
                    title { "Error" }
                },
                view! { cx,
                    p { (format!("Internal server error: '{}'.", err)) }
                },
            ),
        }
    })
}
```

The code above returns an error view depending on the type of error that occurs in the application. You can customize each error view as you see it. You can now add the error view to the main function using the `error_views` method, as shown below:

```rust
mod templates;
use perseus::prelude::*;

#[perseus::main(perseus_axum::dflt_server)]
pub fn main<G: Html>() -> PerseusApp<G> {
    PerseusApp::new()
        .template(crate::templates::index::get_template())
        .error_views(crate::templates::error::get_error_views())
}
```

Once this step is complete, you can now run the `perseus deploy` command. Successful output will look like this: ![Perseus Deploy Command](http://blog.logrocket.com/wp-content/uploads/2023/06/perseus-deploy-command.png) Your application is now ready for deployment! The distribution files are located in the `/pkg` directory. To launch the application in a production environment, you'll need to run the `server` executable found in the same directory. 

If you're using a Unix-based system (like Linux or macOS), navigate to the directory in your terminal and run the server using the following commands:

```bash
cd /pkg
./server
```

If you're on a Windows system, you'll also need to navigate to the `pkg` directory but the command to run the server will be slightly different:

```bash
cd /pkg
server.exe
```

Your app should be live now on port `8080`: ![Deploying Perseus App On Port 8080](http://blog.logrocket.com/wp-content/uploads/2023/06/perseus-app-port8080.png) The output should look like this: ![Output Of Our Todo List](http://blog.logrocket.com/wp-content/uploads/2023/06/output-todo-list.gif)

## Final words

<div>Harnessing the power of Rust, Perseus brings a new dimension to the frontend, something we’ve seen mostly in JavaScript-dominated frameworks like Next, Nuxt.js, and Svelte. Perseus gives Rust developers a compelling option for building robust, safe, predictable, and performant applications.</div>

<div>The best part is that it doesn’t use a virtual DOM for its reactivity, which can lead to a significant increase in performance.  Perseus not only inherits some of the best aspects of existing web frameworks but also strives to surpass them. Perseus went [stable on April 9, 2023](https://github.com/framesurge/perseus/discussions/270) after one year of beta development -- this is just the beginning!</div>

I hope what we’ve learned so far can help make your journey to learning and building applications with Perseus a little easier. For further reading, check out the [Perseus documentation](https://framesurge.sh/perseus/en-US/docs). Happy hacking!

---

## 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](https://lp.logrocket.com/blg/signup). 

[![LogRocket Dashboard Free Trial Banner](https://blog.logrocket.com/wp-content/uploads/2017/03/1d0cd-1s_rmyo6nbrasp-xtvbaxfg.png)](https://lp.logrocket.com/blg/signup)

[LogRocket](https://logrocket.com/signup) 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](https://lp.logrocket.com/blg/signup).