Building a Server-Side Rendered Web App with Rust
Getting Started with Template Engines
Rust’s ecosystem offers a robust selection of template engines, providing developers with a range of options for their projects. In this tutorial, we’ll explore the traditional approach to building web applications using templates with server-side rendering. We’ll focus on Askama, a mature engine based on the popular Jinja project.
Setup and Dependencies
To follow along, you’ll need a recent Rust installation (1.39+) and a web browser. Create a new Rust project, and edit the Cargo.toml file to add the necessary dependencies:
[dependencies]
warp = "0.3.2"
tokio = { version = "1", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
askama = "0.11.1"
uuid = { version = "0.8", features = ["v4"] }
chrono = "0.4.19"
thiserror = "1.0.26"
Data Storage
For our CRUD-like functionality, we’ll use a shared Vec of books protected by a read/write lock and stored in an Arc smart-pointer. This allows us to pass the data between threads.
use std::sync::{Arc, RwLock};
type Books = Arc<rwlock<vec>>;
struct Book {
id: uuid::Uuid,
title: String,
author: String,
}
</rwlock<vec
Defining the Book Structure
We’ll define a Book structure and a useful WebResult type for our warp handlers.
type WebResult = Result<t, std::string::string="">;
struct Book {
id: uuid::Uuid,
title: String,
author: String,
}
</t,>
Creating the First Template
In the handler module, create a data structure for the welcome page.
struct WelcomeTemplate {
title: &'static str,
body: &'static str,
}
The welcome.html file resides in the./templates/ folder and includes header.html and footer.html files.
Welcome Handler
The welcome_handler function creates an instance of the WelcomeTemplate struct, calls render(), and handles errors.
async fn welcome_handler() -> WebResult<warp::reply::html> {
let template = WelcomeTemplate {
title: "Welcome",
body: "Hello, World!",
};
let html = template.render().unwrap();
Ok(warp::reply::html(html))
}
</warp::reply::html
Implementing CRUD Functionality
We’ll define all the handlers for our warp server, including newbookhandler, createbookhandler, editbookhandler, and deletebookhandler. Each handler passes the data memory-based data storage and defines path::param for the edit and delete endpoints.
Listing Books
The listbookshandler uses the book/list.html template, acquires a read-lock on our shared data storage, and passes a reference to the books vector to the template.
async fn list_books_handler(books: Books) -> WebResult<warp::reply::html> {
let books = books.read().unwrap();
let template = ListTemplate { books: &*books };
let html = template.render().unwrap();
Ok(warp::reply::html(html))
}
</warp::reply::html
Editing and Deleting Books
The editbookhandler finds the book with the given ID in our “database” or returns an error.
async fn edit_book_handler(books: Books, id: uuid::Uuid) -> WebResult<warp::reply::html> {
let mut books = books.write().unwrap();
let book = books.iter_mut().find(|b| b.id == id);
//...
}
</warp::reply::html
The deletebook_handler removes the book from the list or returns an error if it isn’t found.
async fn delete_book_handler(books: Books, id: uuid::Uuid) -> WebResult<warp::reply::html> {
let mut books = books.write().unwrap();
let index = books.iter().position(|b| b.id == id);
//...
}
</warp::reply::html
Basic Error Handling
We’ll create a template for showing errors to our users.
struct ErrorTemplate {
message: String,
code: u16,
}
async fn handle_rejection(err: std::string::String) -> WebResult<warp::reply::html> {
let template = ErrorTemplate {
message: err,
code: 500,
};
let html = template.render().unwrap();
Ok(warp::reply::html(html))
}
</warp::reply::html
Wiring Everything Together
We’ll create a warp web server, and if you start this application with cargo run and navigate to http://localhost:8080, you’ll see a fully server-rendered UI you can interact with.
#[tokio::main]
async fn main() -> Result<(), std::io::Error> {
let books = Arc::new(RwLock::new(Vec::new()));
let welcome_handler = warp::path!("").and_then(welcome_handler);
let list_books_handler = warp::path!("books").and_then(list_books_handler);
//...
let routes = welcome_handler.or(list_books_handler).or(/*... */);
warp::serve(routes).run(([127, 0, 0, 1], 8080)).await
}
The Power of Template Engines
Rust’s ecosystem provides many different options for template engines, each with its own trade-offs. By choosing the right engine for your project, you can ensure safe, performant template rendering.
Debugging and Performance Monitoring
When building complex web applications, it’s essential to have proper debugging and performance monitoring tools in place. This allows you to identify and fix issues quickly, ensuring a better user experience.