Web development

Sometimes, a data engineer or scientist needs to write and deploy dynamic websites. Therefore, in this section, I'll give a brief introduction into using Rust's Axum crate for development.

A simple website

Let's start by defining our Cargo.toml file:

1[package]
2name = "axum-basics-html"
3version = "0.1.0"
4edition = "2021"
5
6[dependencies]
7axum = "0.7"
8tokio = { version = "1.21.2", features = ["full"] }

and then our src/main.rs:

1use axum::{
2 response::{Html, IntoResponse},
3 routing::get,
4 Router,
5};
6use tokio::net::TcpListener;
7
8#[tokio::main]
9async fn main() -> Result<(), String> {
10 let app = create_routes().await;
11 let bind_addr = &"0.0.0.0:2900";
12 let listener = TcpListener::bind(bind_addr)
13 .await
14 .map_err(|e| format!("Failed to parse address: {}", e))?;
15 axum::serve(listener, app.into_make_service())
16 .await
17 .map_err(|e| format!("Server error: {}", e))?;
18 Ok(())
19}
20
21pub async fn create_routes() -> Router {
22 Router::new()
23 .route("/", get(index))
24 .route("/some", get(some))
25}
26
27pub async fn index() -> impl IntoResponse {
28 let content = r#"<html><body>
29 <h1>Website index</h1>
30 <p><a href="/some">Go to some page</a></p>
31 </body></html>"#;
32 Html(content)
33}
34
35pub async fn some() -> impl IntoResponse {
36 let hard: i64 = (1..=100).sum();
37 let easy: i64 = (1 + 100) * (100 / 2);
38 let content = format!(
39 r#"<html><body>
40 <h1>Sum of all integer numbers from 1 to 100</h1>
41 <p>The hard way: {}</p>
42 <p>The easy way: {}</p>
43 <p><a href="/">Go to home page</a></p>
44 </body></html>"#,
45 hard, easy
46 );
47 Html(content)
48}

Now cargo run and open http://127.0.0.1:2900/ on your browser.

An API example

It's also quite simple to create APIs with Axum. Once again, let's start by defining our Cargo.toml file:

1[package]
2name = "axum-basics-json"
3version = "0.1.0"
4edition = "2021"
5
6[dependencies]
7axum = "0.7"
8tokio = { version = "1.21.2", features = ["full"] }
9serde = { version = "1.0", features = ["derive"] }
10serde_json = "1.0"

and then our src/main.rs:

1use axum::{
2 extract,
3 response::{IntoResponse, Json},
4 routing::post,
5 Router,
6};
7use serde::{Deserialize, Serialize};
8use tokio::net::TcpListener;
9
10#[derive(Serialize)]
11struct SomeResponse {
12 sum: u32,
13}
14
15#[derive(Deserialize)]
16pub struct SomePayload {
17 // Option is used to make the fields optional
18 max_value: Option<u32>,
19 min_value: Option<u32>,
20}
21
22#[tokio::main]
23async fn main() -> Result<(), String> {
24 let app = create_routes().await;
25 let bind_addr = &"0.0.0.0:2900";
26 let listener = TcpListener::bind(bind_addr)
27 .await
28 .map_err(|e| format!("Failed to parse address: {}", e))?;
29 axum::serve(listener, app.into_make_service())
30 .await
31 .map_err(|e| format!("Server error: {}", e))?;
32 Ok(())
33}
34
35pub async fn create_routes() -> Router {
36 Router::new().route("/api/v1/some", post(some))
37}
38
39pub async fn some(
40 extract::Json(payload): extract::Json<SomePayload>,
41) -> impl IntoResponse {
42 let min_value = payload.min_value.unwrap_or_default();
43 let max_value = payload.max_value.unwrap_or_default();
44 let content = SomeResponse {
45 sum: (min_value..=max_value).sum(),
46 };
47 Json(content)
48}

Now cargo run and try your new API endpoint with some like

curl 'http://127.0.0.1:2900/api/v1/some' -d '{"min_value":0, "max_value":100}' -H 'Content-Type: application/json' -X 'POST'

or the Thunderclient extension for VSCode.

Next steps

My recommendation is to start by checking out my Axum template repo. And once you are familiar enough with it, you can check my Axum OAuth template repo which is an example of combining Axum with SQLx.

Note that if you what you want is a just simple static website, my recommendation goes to Zola (which is used to deploy this website).


If you found this project helpful, please consider making a donation.