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_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_subscriber::fmt::init();
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_subscriber::fmt::init();
let cors = CorsLayer::new()
.allow_origin(Any);
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
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
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);
}
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
#[tokio::main]
async fn main() {
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);
}
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
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"] }
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
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);
}
);
|| ()
})
}
}
web-sys = { version = "0.3.60", features = ["HtmlInputElement"] }
gloo-console = "0.2.3"
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>
}
}
use yew::prelude::*;
use web_sys::HtmlInputElement;
#[function_component]
pub fn Form() -> Html {
let name_ref = NodeRef::default();
let name_ref_outer = name_ref.clone();
let price_ref = NodeRef::default();
let price_ref_outer = price_ref.clone();
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};
async fn handle_error(_err: io::Error) -> impl IntoResponse {
(StatusCode::INTERNAL_SERVER_ERROR, "Something went wrong...")
}
#[tokio::main]
async fn main() {
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);
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
044 - Yew Routing
045 - Yew Routing Adding Components and Navigation