Basic Frontend Templating

Welcome to the final part of our Rust Web series! In the last few part, we've been exploring the Rocket library for Rust web servers. Last time out, we tried a couple ways to add authentication to our web server. In this last part, we'll explore some ideas around frontend templating. This will make it easy for you to serve HTML content to your users!

To explore the code for this article, head over to the "rocket_template" file on our Github repo! If you're still new to Rust, you might want to start with some simpler material. Take a look at our Rust Beginners Series as well!

Templating Basics

First, let's understand the basics of HTML templating. When our server serves out a webpage, we return HTML to the user for the browser to render. Consider this simple index page:

<html>
  <head></head>
  <body>
    <p> Welcome to the site!</p>
  </body>
</html>

But of course, each user should see some kind of custom content. For example, in our greeting, we might want to give the user's name. In an HTML template, we'll create a variable or sorts in our HTML, delineated by braces:

<html>
  <head></head>
  <body>
    <p> Welcome to the site {{name}}!</p>
  </body>
</html>

Now before we return the HTML to the user, we want to perform a substitution. Where we find the variable {{name}}, we should replace it with the user's name, which our server should know.

There are many different libraries that do this, often through Javascript. But in Rust, it turns out the Rocket library has a couple easy templating integrations. One option is Tera, which was specifically designed for Rust. Another option is Handlebars, which is more native to Javascript, but also has a Rocket integration. The substitutions in this article are simple, so there's not actually much of a difference for us.

Returning a Template

So how do we configure our server to return this HTML data? To start, we have to attach a "Fairing" to our server, specifically for the Template library. A Fairing is a server-wide piece of middleware. This is how we can allow our endpoints to return templates:

use rocket_contrib::templates::Template;

fn main() {
  rocket::ignite()
    .mount("/", routes![index, get_user])
    .attach(Template::fairing())
    .launch();
}

Now we can make our index endpoint. It has no inputs, and it will return Rocket's Template type.

#[get("/")]
fn index() -> Template {
    ...
}

We have two tasks now. First, we have to construct our context. This can be any "map-like" type with string information. We'll use a HashMap, populating the name value.

#[get("/")]
fn index() -> Template {
    let context: HashMap<&str, &str> = [("name", "Jonathan")]
        .iter().cloned().collect();
    ...
}

Now we have to render our template. Let's suppose we have a "templates" directory at the root of our project. We can put the template we wrote above in the "index.hbs" file. When we call the render function, we just give the name of our template and pass the context!

#[get("/")]
fn index() -> Template {
    let context: HashMap<&str, &str> = [("name", "Jonathan")]
        .iter().cloned().collect();
    Template::render("index", &context)
}

Including Static Assets

Rocket also makes it quite easy to include static assets as part of our routing system. We just have to mount the static route to the desired prefix when launching our server:

fn main() {
    rocket::ignite()
        .mount("/static", StaticFiles::from("static"))
        .mount("/", routes![index, get_user])
        .attach(Template::fairing())
        .launch();
}

Now any request to a /static/... endpoint will return the corresponding file in the "static" directory of our project. Suppose we have this styles.css file:

p {
  color: red;
}

We can then link to this file in our index template:

<html>
  <head>
    <link rel="stylesheet" type="text/css" href="static/styles.css"/>
  </head>
  <body>
    <p> Welcome to the site {{name}}!</p>
  </body>
</html>

Now when we fetch our index, we'll see that the text on the page is red!

Looping in our Database

Now for one last piece of integration with our database. Let's make a page that will show a user their basic information. This starts with a simple template:

<!-- templates/user.hbs -->
<html>
  <head></head>
  <body>
    <p> User name: {{name}}</p>
    <br>
    <p> User email: {{email}}</p>
    <br>
    <p> User name: {{age}}</p>
  </body>
</html>

We'll compose an endpoint that takes the user's ID as an input and fetches the user from the database:

#[get("/users/<uid>")]
fn get_user(uid: i32) -> Template {
    let maybe_user = fetch_user_by_id(&local_conn_string(), uid);
    ...
}

Now we need to build our context from the user information. This will require a match statement on the resulting user. We'll use Unknown for the fields if the user doesn't exist.

#[get("/users/<uid>")]
fn get_user(uid: i32) -> Template {
    let maybe_user = fetch_user_by_id(&local_conn_string(), uid);
    let context: HashMap<&str, String> = {
        match maybe_user {
            Some(u) =>
              [ ("name", u.name.clone())
              , ("email", u.email.clone())
              , ("age", u.age.to_string())
              ].iter().cloned().collect(),
            None =>
              [ ("name", String::from("Unknown"))
              , ("email", String::from("Unknown"))
              , ("age", String::from("Unknown"))
              ].iter().cloned().collect()
        }
    };
    Template::render("user", &context)
}

And to wrap it up, we'll render the "user" template! Now when users get directed to the page for their user ID, they'll see their information!

Conclusion

That's all for our Rust Web series! Hopefully you learned a good amount, and feel confident enough to take on some bigger challenges in Rust. For a more in-depth introduction to Rust basics, be sure to take a look at our Rust Video Tutorial!