iT邦幫忙

2025 iThome 鐵人賽

DAY 9
0
Rust

Rust 後端入門系列 第 9

Day 9 Axum GET/POST:打造 RESTful API 的第一步

  • 分享至 

  • xImage
  •  

今天你會掌握:

  • 路由 : 理解 Axum 如何處理不同的請求
  • GET 請求處理 : 學會接收和回應查詢請求
  • POST 請求處理 : 處理客戶端發送的資料
  • 實戰 : 建立一個真正可用的 RESTful API

無論是開發手機 App 後端、微服務,還是現代化的網頁應用,這些都是必備技能。

GET和POST有什麼差別?

用餐廳當比喻:

GET: 客人看菜單

  • 只是獲取資訊:不會改變餐廳的任何東西
  • 可以重複做:看100次菜單都不會有問題
  • 公開透明:別人可以看到你在看什麼(URL顯示參數)

POST:客人直接點餐

  • 會改變狀態:廚房會開始準備食物,庫存會減少
  • 不應重複:重複點餐會造成問題(重複扣款、重複訂單)
  • 私密性較高:訂單內容不會公開顯示

深入理解路由

什麼是路由?

用生活化的例子幫助理解,路由就像大樓的電梯系統:

  • 按 1 樓 → 到大廳(首頁)
  • 按 5 樓 → 到辦公室(用戶頁面)
  • 按 10 樓 → 到餐廳(API 端點)

每個按鈕(路徑)都對應特定目的地(處理函數)

Axum 路由的核心架構

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

// 路由的三要素:
// 1. 路徑 (Path) : "/users"
// 2. 方法 (Method) ; GET, POST, PUT, DELETE等
// 3. 處理器 (Handler) : 實際執行的函數

let app = Router::new()
    .route("/", get(home)) // 路徑 "/" + GET 方法 + home 處理器
    .route("/api/users", get(list_users).post(create_user)); // 一個路徑可以有多個方法!

GET

讓我們從最簡單的範例開始

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

#[tokio::main]
async fn main() {
    let app = Router::new()
        .route("/", get(home))
        .route("/about", get(about));

    let listener = tokio::net::TcpListener::bind("127.0.0.1:3000")
        .await
        .unwrap();
    
    println!("服務器運作中 http://127.0.0.1:3000");
    axum::serve(listener, app).await.unwrap();
}

async fn home() -> &'static str {
    "歡迎來到首頁!"
}

async fn about() -> &'static str {
    "這是關於頁面"
}

相當單純易懂,大家應該能掌握程式如何運作。

可以瀏覽 http://127.0.0.1:3000/http://127.0.0.1:3000/about,看到對應的內容。

接下來,再來一個稍微複雜的範例,讓大家看看如何使用GET方法傳遞路徑參數:

use axum::{
    Router,
    routing::get,
    extract::Path,  // 用於提取路徑參數
};

#[tokio::main]
async fn main() {
    let app = Router::new()
        // {id} 是動態參數,隨著輸入的內容改變
        .route("/user/{id}", get(get_user))
        .route("/post/{year}/{month}/{day}", get(get_post));

    println!(" 可以瀏覽以下網址查看結果:");
    println!("  http://127.0.0.1:3000/user/123");
    println!("  http://127.0.0.1:3000/post/2025/09/23");
    
    let listener = tokio::net::TcpListener::bind("127.0.0.1:3000")
        .await
        .unwrap();
    
    axum::serve(listener, app).await.unwrap();
}

// 提取一個參數的Handler
async fn get_user(Path(id): Path<u32>) -> String {
    format!("你正在查看用戶 #{} 的資料", id)
}

// 提取多個參數的Handler
async fn get_post(
    Path((year, month, day)): Path<(u32, u32, u32)>
) -> String {
    format!("查看 {}/{}/{} 的文章", year, month, day)
}

接著是一個查詢參數的範例:

use axum::{
    Router,
    routing::get,
    extract::{Path, Query},
};
use serde::Deserialize;  // 需要在 Cargo.toml 添加 serde = { version = "1.0", features = ["derive"] }

// 定義查詢參數結構
#[derive(Deserialize)]
struct Pagination {
    page: Option<u32>,
    limit: Option<u32>,
}

#[tokio::main]
async fn main() {
    let app = Router::new()
        .route("/products", get(list_products))
    
    let listener = tokio::net::TcpListener::bind("127.0.0.1:3000")
        .await
        .unwrap();
    
    axum::serve(listener, app).await.unwrap();
}

// 查詢參數
async fn list_products(Query(params): Query<Pagination>) -> String {
    let page = params.page.unwrap_or(1);
    let limit = params.limit.unwrap_or(10);
    
    format!(
        "顯示產品列表\n頁數: {}\n每頁數量: {}", 
        page, limit
    )
}

我們可以瀏覽 127.0.0.1:3000/products?page=1&limit=10 來測試這個程式,顯示的結果是

顯示產品列表
頁數: 1
每頁數量: 10

你可能會說這兩個範例幾乎是一樣的,但它們有點不同。

路徑參數 vs 查詢參數

雖然兩者都是傳遞資料的方式,但它們在 URL 結構和使用場景上有明顯區別:

  • URL 位置不同
// 路徑參數:在 URL 路徑中
GET /user/123                     // 123 是路徑參數
GET /post/2025/09/23              // 2025, 09, 23 都是路徑參數

// 查詢參數:在 ? 後面,用 & 分隔
GET /products?page=2&limit=10     // page=2 和 limit=10 是查詢參數
  • 差異對比
特性 路徑參數 (Path) 查詢參數 (Query)
URL 格式 /user/:id/user/123 /products?page=1&limit=10
必要性 必需填入內容 可以省略
用途 識別特定資源 過濾、排序、分頁

POST

範例:

use axum::{
    Router,
    routing::{get, post},
    Json,  // 用來處理 JSON
};
use serde::{Deserialize, Serialize};

// 定義請求和回應的資料結構
#[derive(Deserialize)]  // 用來接收資料(反序列化)
struct User {
    username: String,
    email: String,
    password: String,
}

#[derive(Serialize)]  // 用來回傳資料(序列化)
struct UserResponse {
    id: u32,
    username: String,
    message: String,
}

#[tokio::main]
async fn main() {
    let app = Router::new()
        .route("/users", post(create_user));

    let listener = tokio::net::TcpListener::bind("127.0.0.1:3000")
        .await
        .unwrap();
    
    axum::serve(listener, app).await.unwrap();
}

async fn create_user(Json(payload): Json<User>) -> Json<UserResponse> {
    // 在實際應用程式,會在資料庫執行操作,但我們還沒有資料庫,先用一個println代替
    println!("收到新用戶註冊:{}", payload.username);
    
    // 回傳 JSON 回應
    Json(UserResponse {
        id: 123,  // 我們尚未連接資料庫,先假裝是資料庫產生的 id
        username: payload.username,
        message: format!("歡迎加入!你的信箱是 {}", payload.email),
    })
}
  • User:用來接收用戶端發送的 JSON 格式註冊資料
  • UserResponse:用來回傳註冊成功的 JSON 回應

我們可以用curl或postman來測試我們的API是否正常運作

curl -X POST http://127.0.0.1:3000/users -H "Content-Type: application/json" -d "{\"username\":\"user1\",\"email\":\"user1@aaa.com\",\"password\":\"12345678\"}"

我們應該可以得到以下的JSON回應

{"id":123,"username":"user1","message":"歡迎加入!你的信箱是 user1@aaa.com"}



上一篇
Day8 Axum 入門:打造第一個 Rust Web 應用
系列文
Rust 後端入門9
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言