A straightforward tutorial on how to setup a Rust based web app with actix and basic templating functionalities. A useless endeavour in post GPT era. I assure you all of this is thought out and planned by a human who wasted some human hours on this.
The tools for the trade
- Actix : Web framework for Rust
- Tera : Templating engine that is one to one copy of Jinja Templating engine
- Nginx : HTTP Server
Cargo.toml
actix-files = "0.6.2"
actix-web = "4.4.0"
lazy_static = "1.4.0"
tera = "1.19.1"
Discovering Actix
A simple HTTP server that responds with “Hello World”. Look through Getting started with Actix for more information.
// Don't copy this code, just for example purposes
use actix_web::{get, App,HttpResponse, HttpServer, Responder};
use actix_files as fs;
#[get("/")]
async fn hello() -> impl Responder {
HttpResponse::Ok().body("Hello World")
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| {
App::new()
.service(hello)
})
.bind(("127.0.0.1", 8080))?
.run()
.await
}
Templating
One can just open up a file using include_str!
, then serve the string through HttpResponse
. If dynamic content is needed then we can always manipulate the string, but that is a hassle. A little touch of metaprogramming won’t hurt here. We can manage server side state straight forward. But we don’t want statefulness and virtual-dom like React. Our main goal here is to achieve performance while understanding every bits of abstraction without making the program bloated. So i chose Tera.
Suppose Templates directory has the following format.
templates/
countdown.html
base.html
products/
product.html
price.html
The contents of base.html
The base of our templates. For for information about templating refer to jinja2 documentation
// copy this code and save it as templates/base.html
<!DOCTYPE html>
<html lang="en">
<head>
{% block head %}
<link rel="stylesheet" href="./style.css" />
<title>{% block title %}{% endblock title %}</title>
{% endblock head %}
</head>
<body>
<div id="content">{% block content %}{% endblock content %}</div>
<div id="footer">
{% block footer %}
© Copyright 2023 <a href="https://thapa-ashish.com.np">Ashish Thapa</a>.
{% endblock footer %}
</div>
</body>
</html>
Lets focus on Blocks
Blocks are used for inheritance and act as both placeholders and replacements at the same time.
{% block head %}
<link rel="stylesheet" href="./style.css" />
<title>{% block title %}{% endblock title %}</title>
{% endblock head %}
Later other templates can use this as a ground source of truth and extend from the template.
Now lets define a child template countdown.html
//save it on templates/countdown.html
{% extends "base.html" %}
{% block title %}Index{% endblock title %}
{% block head %}
{{ super() }}
<style type="text/css">
.important { color: #336699; }
</style>
{% endblock head %}
{% block content %}
<h1>Index</h1>
<p class="important">It's a final countdown</p>
{% endblock content %}
We can go through all the templates with
#[macro_use]
extern crate lazy_static;
// code is executed at runtime to be initialized when one uses lazy_static
lazy_static! {
pub static ref TEMPLATES: Tera = {
let mut tera = match Tera::new("templates/**/*") {
Ok(t) => t,
Err(e) => {
println!("Parsing error(s): {}", e);
::std::process::exit(1);
}
};
// to understand why autoescaping is done go
// https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html
tera.autoescape_on(vec![".html"]);
tera.register_filter("do_nothing", do_nothing_filter);
tera
};
}
Connecting the dots index.html
// you can copy this code
#[macro_use]
extern crate lazy_static;
extern crate tera;
use tera::{Tera,Context};
lazy_static!{
pub static ref TEMPLATES: Tera = {
let mut tera = match Tera::new("./templates/**/*") {
Ok(t) => t,
Err(e) => {
println!("Parsing error(s): {}", e);
::std::process::exit(1);
}
};
tera.autoescape_on(vec![".html", ".sql"]);
tera
};
}
use actix_web::{get, App,HttpResponse, HttpServer, Responder};
use actix_files as fs;
#[get("/")]
async fn hello() -> impl Responder {
let context = Context::new();
let response = TEMPLATES.render("countdown.html",&context).unwrap();
HttpResponse::Ok().body(response)
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| {
App::new()
//we also want static files like css files to be exposed by the server.
.service(fs::Files::new("/static","./static/").show_files_listing())
.service(hello)
})
.bind(("127.0.0.1", 8080))?
.run()
.await
}
Once done, try running the app with cargo run
, and if successful, we can get to setting up setting Nginx.
Create a systemd service for the app
Create a file named /etc/systemd/system/rust-app-countdown.service with the following content:
[Unit]
Description=rust-app
[Service]
ExecStart=/path/to/your/my_rust_app
WorkingDirectory=/path/to/your
User=your_username
Restart=always
Enable and start the service with
sudo systemctl enable rust-app-countdown
sudo systemctl start rust-app-countdown
Nginx setup
Install nginx
sudo apt update
sudo apt install nginx
sudo ufw allow 80
// create a server block
sudo nano /etc/nginx/sites-available/rust-app-countdown
Add the following configuration on the file rust-app-countdown
server {
listen 80;
server_name your_domain_or_droplet_ip;
location / {
proxy_pass http://localhost:your_rust_app_port;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}
Lastly create a symbolic link, verify the configuration and restart nginx
sudo ln -s /etc/nginx/sites-available/my_rust_app /etc/nginx/sites-enabled
sudo nginx -t
sudo systemctl restart nginx