Learn Full Stack Rust Programming using AXUM Yew and SQLx

introduction

2 - Build a rust REST API using AXUM SQLx and Postgres

020 - Setting Up AXUM Server

[package]
name = "server"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
tokio = { version = "1.25.0",features = ["full"] }
tracing = "0.1.37"
tracing-subscriber = "0.3.16"
axum = "0.6.4"
use axum::{
    routing::{get}, Router,
};

// 处理函数
async fn root() -> &'static str {
    "hello world!"
}

#[tokio::main]
async fn main() {
    // tracing
    tracing_subscriber::fmt::init();

    let app = Router::new()
        .route("/", get(root));

    tracing::debug!("listening on port {}","0.0.0.0:3001");
    println!("listening on port {}","0.0.0.0:3001");

    axum::Server::bind(&"0.0.0.0:3001".parse().unwrap())
        .serve(app.into_make_service())
        .await
        .unwrap();
}

021 - Add Cargo Watch

# 用于热更新
cargo add cargo-watch
# 安装到本地
cargo install cargo-watch
# 监听启动
cargo-watch -x run

022 - Adding CORS to AXUM

use axum::{
    routing::{get}, Router,
};

// 处理函数
async fn root() -> &'static str {
    "hello world!"
}

use tower_http::cors::{Any, CorsLayer};

#[tokio::main]
async fn main() {
    // tracing
    tracing_subscriber::fmt::init();

    // add cors
    let cors = CorsLayer::new()
        .allow_origin(Any);

    let app = Router::new()
        .route("/", get(root))
        .layer(cors);

    tracing::debug!("listening on port {}","0.0.0.0:3001");
    println!("listening on port {}", "0.0.0.0:3001");

    axum::Server::bind(&"0.0.0.0:3001".parse().unwrap())
        .serve(app.into_make_service())
        .await
        .unwrap();
}

023 - Adding PostgresSQLx

[package]
name = "server"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
tokio = { version = "1.25.0", features = ["full"] }
tracing = "0.1.37"
tracing-subscriber = "0.3.16"
axum = "0.6.4"
tower = "0.4.13"
tower-http = { version = "0.3.5", features = ["full"] }
dot-env = "0.15.0"
sqlx = { version = "0.6.2", features = ["runtime-tokio-rustls", "json", "postgres", "macros"] }
use axum::{
    routing::{get}, Router,
};

// 处理函数
async fn root() -> &'static str {
    "hello world!"
}

use tower_http::cors::{Any, CorsLayer};

#[tokio::main]
async fn main() {
    // tracing
    tracing_subscriber::fmt::init();

    // add cors
    let cors = CorsLayer::new()
        .allow_origin(Any);

    // add postgres
    dotenv::dotenv().ok();
    let db_url = std::env::var("DATABASE_URL")
        .expect("DATABASE URL not set");
    let pool = sqlx::PgPool::connect(&db_url)
        .await
        .expect("Error with pool connection");

    let app = Router::new()
        .route("/", get(root))
        .with_state(pool)
        .layer(cors);

    tracing::debug!("listening on port {}","0.0.0.0:3001");
    println!("listening on port {}", "0.0.0.0:3001");

    axum::Server::bind(&"0.0.0.0:3001".parse().unwrap())
        .serve(app.into_make_service())
        .await
        .unwrap();
}
// .env
DATABASE_URL=postgresql://username:password@localhost:5432/rustcourse

024 - Adding Postgres Table

    // add postgres table
    sqlx::query(
        r#"
        CREATE TABLE IF NOT EXISTS products (
            id serial,
            name text,
            price integer
        );
        "#
    )
        .execute(&pool)
        .await.expect("create table error!");

025 - AXUM API Create Product

### 添加product
POST http://localhost:3001/api/products
Content-Type: application/json

{
  "name": "mi",
  "price": 12
}

<> 2023-12-31T194251.200.json
<> 2023-12-31T194240.422.txt
<> 2023-12-31T193947.400.txt
<> 2023-12-31T193940.415.txt
<> 2023-12-31T193926.415.txt
mod handlers;

// ...

#[tokio::main]
async fn main() {
    // ...

    let app = Router::new()
        .route("/", get(root))
        .route("/api/products", post(handlers::create_product))
        .with_state(pool)
        .layer(cors);

    // ...
}
// server/src/handlers.rs
use axum::{
    extract::State,
    http::StatusCode,
    Json,
};

use sqlx::postgres::PgPool;
use serde_json::{json, Value};
use serde::{Deserialize, Serialize};

#[derive(Deserialize, Serialize)]
pub struct NewProduct {
    name: String,
    price: i32,
}

pub async fn create_product(State(pool): State<PgPool>, Json(product): Json<NewProduct>)
                      -> Result<Json<Value>, StatusCode> {
    let result = sqlx::query("INSERT INTO products (name, price) values ($1, $2)")
        .bind(&product.name)
        .bind(&product.price)
        .execute(&pool)
        .await
        .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
    Ok(Json(json!(product)))
}

026 - AXUM API Get ALL Products

// main.rs
// ...

#[tokio::main]
async fn main() {
    // ...

    // add postgres table
    // sqlx::query(
    //     r#"
    //     CREATE TABLE IF NOT EXISTS products (
    //         id serial,
    //         name text,
    //         price integer
    //     );
    //     "#
    // )
    //     .execute(&pool)
    //     .await.expect("create table error!");

    let app = Router::new()
        .route("/", get(root))
        .route("/api/products", post(handlers::create_product))
        .route("/api/products", get(handlers::get_products))
        .with_state(pool)
        .layer(cors);

    // ...
}
// handlers.rs
// 查询products
pub async fn get_products(State(pool): State<PgPool>)
                          -> Result<Json<Vec<Product>>, (StatusCode, String)> {
    let result = sqlx::query_as("SELECT * FROM products")
        .fetch_all(&pool)
        .await
        .map_err(|err|
            (
                StatusCode::INTERNAL_SERVER_ERROR,
                format!("Something is wrong {}", err)
            )
        )?;
    Ok(Json(result))
}

027 - AXUM API Get ONE product

    let app = Router::new()
        .route("/", get(root))
        .route("/api/products", post(handlers::create_product))
        .route("/api/products", get(handlers::get_products))
        .route("/api/products/:id", get(handlers::get_one_product))
        .with_state(pool)
        .layer(cors);
// 查询单个
pub async fn get_one_product(
    State(pool): State<PgPool>,
    // 路径参数
    axum::extract::Path(id): axum::extract::Path<i32>,
)
    -> Result<Json<Product>, (StatusCode, String)> {
    let result = sqlx::query_as("SELECT id, name, price FROM products WHERE id = $1")
        .bind(id)
        .fetch_one(&pool)
        .await
        .map_err(|err|
            match err {
                sqlx::Error::RowNotFound =>
                    (
                        StatusCode::NOT_FOUND,
                        format!("Error is {}", err)
                    ),
                _ =>
                    (
                        StatusCode::INTERNAL_SERVER_ERROR,
                        format!("Something is wrong {}", err)
                    )
            }
        )?;
    Ok(Json(result))
}

028 - AXUM API Update Product Coming soon

029 - AXUM API Delete Product

    let app = Router::new()
        .route("/", get(root))
        .route("/api/products", post(handlers::create_product))
        .route("/api/products", get(handlers::get_products))
        .route("/api/products/:id", get(handlers::get_one_product))
        .route("/api/products/:id", delete(handlers::del_one_product))
        .with_state(pool)
        .layer(cors);
// 删除单个
pub async fn del_one_product(
    State(pool): State<PgPool>,
    // 路径参数
    axum::extract::Path(id): axum::extract::Path<i32>,
)
    -> Result<Json<Value>, (StatusCode, String)> {
    let result = sqlx::query(
        "DELETE FROM products WHERE id = $1")
        .bind(id)
        .execute(&pool)
        .await
        .map_err(|err|
            match err {
                sqlx::Error::RowNotFound =>
                    (
                        StatusCode::NOT_FOUND,
                        format!("Error is {}", err)
                    ),
                _ =>
                    (
                        StatusCode::INTERNAL_SERVER_ERROR,
                        format!("Something is wrong {}", err)
                    )
            }
        )?;
    Ok(Json(json!({
        "msg":"product deleted successfully"
    })))
}

030 - AXUM API Delete product continued

031 - Server API completed see final code

3 - Frontend with Yew Framework

032 - Yew Frontend

033 - Setting Up Yew

cargo new --bin frontend
cargo install --locked trunk
[dependencies]
yew = { version = "0.20.0", features = ["csr"] }
<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
</html>
use yew::prelude::*;

#[function_component]
fn App() -> Html {
    let name = "Zoe";

    html! {
        <div>
            <h1>{"Yew ProductsApp"}</h1>
            <p>{name}</p>
        </div>
    }
}

fn main() {
    yew::Renderer::<App>::new().render();
}
# 会监听变化
trunk serve --port 18011

034 - Adding Styling to Yew

:root {
    --primary: rgb(255, 200, 10);
    --light-color: rgb(235, 235, 235);
    --dark-color: rgb(5, 5, 30);
}

html, body {
    padding: 0;
    margin: 0;
    background: var(--dark-color);
    color: var(--light-color);
    font-family: Arial, Helvetica, sans-serif;
}

.container {
    width: 90vw;
    min-height: 90vh;
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
}

.title {
    color: var(--primary);
    font-size: 2.5rem;
}
#[function_component]
fn App() -> Html {
    let name = "Zoe";

    html! {
        <div class="container">
            <h1 class="title">{"Yew ProductsApp"}</h1>
            <p>{name}</p>
        </div>
    }
}
<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
    <link data-trunk rel="css" href="style.css">
</head>
</html>

035 - Yew Components

// frontend/src/products.rs
use yew::prelude::*;

#[function_component]
pub fn Products() -> Html {

    html! {
        <div class="container">
            <h2>{"List of Products"}</h2>
        </div>
    }
}
mod form;
mod products;

use yew::prelude::*;
use products::Products;

#[function_component]
fn App() -> Html {
    let name = "Zoe";

    html! {
        <div class="container">
            <h1 class="title">{"Yew ProductsApp"}</h1>
            <Products />
            <p></p>
        </div>
    }
}

fn main() {
    yew::Renderer::<App>::new().render();
}

036 - API Get Request

[dependencies]
yew = { version = "0.20.0", features = ["csr"] }
serde = { version = "1.0.152", features = ["derive"] }
wasm-bindgen-futures = "0.4.33" 
reqwest = { version = "0.11.14", features = ["json"] }
// frontend/src/products.rs
use yew::prelude::*;
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Debug)]
pub struct Product {
    id: i32,
    name: String,
    price: i32,
}

#[function_component]
pub fn Products() -> Html {
    let data: UseStateHandle<Vec<Product>> = use_state(|| vec![]);
    let data_clone = data.clone();
    wasm_bindgen_futures::spawn_local(
        async move {
            let fetched_data = reqwest::get("http://localhost:3001/api/products")
                .await
                .expect("cannot get data from url")
                .json::<Vec<Product>>()
                .await
                .expect("cannot convert to json");
            data_clone.set(fetched_data);
        }
    );

    html! {
        <div class="container">
            <h2>{"List of Products: "} {data.len()}</h2>
        </div>
    }
}

037 - Displaying Data

// frontend/src/products.rs
use yew::prelude::*;
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Debug)]
pub struct Product {
    id: i32,
    name: String,
    price: i32,
}

#[function_component]
pub fn Products() -> Html {
    let data: UseStateHandle<Vec<Product>> = use_state(|| vec![]);
    let data_clone = data.clone();
    wasm_bindgen_futures::spawn_local(
        async move {
            let fetched_data = reqwest::get("http://localhost:3001/api/products")
                .await
                .expect("cannot get data from url")
                .json::<Vec<Product>>()
                .await
                .expect("cannot convert to json");
            data_clone.set(fetched_data);
        }
    );

    let products = data.iter().map(|product| html! {
        <ul>
            <li key={product.id}>{format!("Name: {}, Price: {}",product.name,product.price)}</li>
        </ul>
    }).collect::<Html>();

    html! {
        <div class="container">
            <h2>{"List of Products: "} {data.len()}</h2>
            <p>{products}</p>
        </div>
    }
}

038 - Yew useeffect hook

#[function_component]
pub fn Products() -> Html {
    let data: UseStateHandle<Vec<Product>> = use_state(|| vec![]);
    {
        let data_clone = data.clone();
        use_effect(move || {
            wasm_bindgen_futures::spawn_local(
                async move {
                    let fetched_data = reqwest::get("http://localhost:3001/api/products")
                        .await
                        .expect("cannot get data from url")
                        .json::<Vec<Product>>()
                        .await
                        .expect("cannot convert to json");
                    data_clone.set(fetched_data);
                }
            );
            || ()
        })
    }

    // ...
}

039 - Create Form

web-sys = { version = "0.3.60", features = ["HtmlInputElement"] }
gloo-console = "0.2.3"
// frontend/src/products.rs
use yew::prelude::*; 
use web_sys::HtmlInputElement; 

#[function_component]
pub fn Form() -> Html {

    html! {
        <div class="container">
            <h2>{"Add a Product"}</h2>
            <div>

            </div>
        </div>
    }
}

040 - Add Form

// frontend/src/products.rs
use yew::prelude::*;
use web_sys::HtmlInputElement;

#[function_component]
pub fn Form() -> Html {
    // name
    let name_ref = NodeRef::default();
    let name_ref_outer = name_ref.clone();

    // price
    let price_ref = NodeRef::default();
    let price_ref_outer = price_ref.clone();

    // submit form data
    let onclick = Callback::from(move |_| {
        gloo_console::log!("Button Clicked");
    });

    html! {
        <div class="container">
            <h2>{"Add a Product"}</h2>
            <div>
                <label for="name" class="">
                {"Name"}
                <input
                    ref={name_ref_outer.clone()}
                    id="name"
                    class=""
                    type="text"
                    />
                </label>
                <br/> <br/>
                <label for="price" class="">
                {"Price"}
                <input
                    ref={price_ref_outer.clone()}
                    id="price"
                    class=""
                    type="number"
                    />
                </label>
                <br/> <br/>
                <button {onclick} class="">{"Add Product"}</button>
            </div>
            <hr />
        </div>
    }
}

041 - Connect Yew and Axum

[workspace]

members = [
    "server", "frontend"
]
# ================================== #

[build]
target = "index.html"
dist = "../dist"

mod handlers;

use axum::{
    response::IntoResponse,
    http::StatusCode,
    routing::{get, post, delete}, Router,
};
use std::io;
use axum::routing::get_service;

// ...

use tower_http::cors::{Any, CorsLayer};
use tower_http::services::{ServeDir, ServeFile};

// add handle error fn
async fn handle_error(_err: io::Error) -> impl IntoResponse {
    (StatusCode::INTERNAL_SERVER_ERROR, "Something went wrong...")
}

#[tokio::main]
async fn main() {
    // add cors ...

    // serving frontend static files
    let serve_dir = ServeDir::new("../frontend/dist")
        .not_found_service(ServeFile::new("../dist/frontend/index.html"));
    let serve_dir = get_service(serve_dir).handle_error(handle_error);

    // add postgres ...

    let app = Router::new()
        .route("/home", get(root))
        .layer(cors)
        .route("/api/products", post(handlers::create_product))
        .route("/api/products", get(handlers::get_products))
        .route("/api/products/:id", get(handlers::get_one_product))
        .route("/api/products/:id", delete(handlers::del_one_product))
        .with_state(pool)
        .nest_service("/", serve_dir.clone())
        .fallback_service(serve_dir.clone());

    // ...
}

042 - Post Form Data to Server API

043 - Style Form

044 - Yew Routing

045 - Yew Routing Adding Components and Navigation


  目录