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]
2 name = "axum-basics-html"
3 version = "0.1.0"
4 edition = "2021"
5
6 [dependencies]
7 axum = "0.7"
8 tokio = { version = "1.21.2", features = ["full"] }
and then our src/main.rs
:
1 use axum::{
2 response::{Html, IntoResponse},
3 routing::get,
4 Router,
5 };
6 use tokio::net::TcpListener;
7
8 #[tokio::main]
9 async 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
21 pub async fn create_routes() -> Router {
22 Router::new()
23 .route("/", get(index))
24 .route("/some", get(some))
25 }
26
27 pub 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
35 pub 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]
2 name = "axum-basics-json"
3 version = "0.1.0"
4 edition = "2021"
5
6 [dependencies]
7 axum = "0.7"
8 tokio = { version = "1.21.2", features = ["full"] }
9 serde = { version = "1.0", features = ["derive"] }
10 serde_json = "1.0"
and then our src/main.rs
:
1 use axum::{
2 extract,
3 response::{IntoResponse, Json},
4 routing::post,
5 Router,
6 };
7 use serde::{Deserialize, Serialize};
8 use tokio::net::TcpListener;
9
10 #[derive(Serialize)]
11 struct SomeResponse {
12 sum: u32,
13 }
14
15 #[derive(Deserialize)]
16 pub 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]
23 async 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
35 pub async fn create_routes() -> Router {
36 Router::new().route("/api/v1/some", post(some))
37 }
38
39 pub 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.