iT邦幫忙

2025 iThome 鐵人賽

DAY 14
0
Rust

Rust 後端入門系列 第 14

Day 14 Axum 專案:簡單 RESTful API

  • 分享至 

  • xImage
  •  

目標

  • 建立簡單的 RESTful API(單一檔案 main.rs),不連資料庫,只用記憶體儲存
  • 支援列出所有任務、取得單一任務、建立任務、更新任務、刪除任務
  • 使用 Axum 、tokio 非同步
  • 使用 thread-safe 的共享狀態(Arc + RwLock / AtomicU64)

為什麼用這些技術?

  • Axum:現代 async web 框架,整合良好且與 tower 生態相容。
  • Arc + RwLock:多執行緒環境下安全共享記憶體狀態,讀多寫少時 RwLock 可以提供較好效能。
  • AtomicU64:產生唯一整數 id(比用 uuid 簡單,適合 demo)。
  • serde / serde_json:JSON 序列化與反序列化。
  • tokio:非同步執行緒池,axum 必要。

開始實做

首先建立專案

cargo new axum_restful_api

Cargo.toml(專案依賴)

[package]
name = "axum_restful_api"
version = "0.1.0"
edition = "2024"

[dependencies]
axum = "0.8"
tokio = { version = "1", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tracing = "0.1"
tracing-subscriber = "0.3"
parking_lot = "0.12"

main.rs(使用 Arc + RwLock 與 AtomicU64 產生 id)

use std::collections::HashMap;
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};

use tokio::sync::RwLock;

use axum::{
    extract::{Path, Extension},
    http::StatusCode,
    response::IntoResponse,
    Json, Router, routing::{get, post, put, delete},
};

use serde::{Deserialize, Serialize};
use tracing_subscriber;

#[derive(Clone, Serialize, Deserialize)]
struct Task {
    id: u64,
    title: String,
    description: Option<String>,
    completed: bool,
}

#[derive(Deserialize)]
struct CreateTask {
    title: String,
    description: Option<String>,
}

#[derive(Deserialize)]
struct UpdateTask {
    title: Option<String>,
    description: Option<String>,
    completed: Option<bool>,
}

// 共享狀態型別:HashMap<id, Task>
type Db = Arc<RwLock<HashMap<u64, Task>>>;
type IdCounter = Arc<AtomicU64>;

#[tokio::main]
async fn main() {
    // 初始化 tracing 用於除錯 / 日誌
    tracing_subscriber::fmt::init();

    // 建立空的記憶體資料庫
    let db: Db = Arc::new(RwLock::new(HashMap::new()));
    let id_counter = Arc::new(AtomicU64::new(1));

    // 建立路由並把共享狀態透過 Extension 注入 handler
    let app = Router::new()
        .route("/tasks", get(list_tasks).post(create_task))
        .route("/tasks/{id}", get(get_task).put(update_task).delete(delete_task))
        .layer(Extension(db.clone()))
        .layer(Extension(id_counter.clone()));

    // 啟動服務(監聽 3000)
    tracing::info!("Starting server at http://127.0.0.1:3000");
    let listener = tokio::net::TcpListener::bind("127.0.0.1:3000")
	        .await
	        .unwrap();
	
	    axum::serve(listener, app)
	        .await
	        .unwrap();
}

// Handler:列出所有任務
async fn list_tasks(Extension(db): Extension<Db>) -> impl IntoResponse {
    let map = db.read().await;
    let tasks: Vec<Task> = map.values().cloned().collect();
    (StatusCode::OK, Json(tasks))
}

// Handler:取得單一任務
async fn get_task(Path(id): Path<u64>, Extension(db): Extension<Db>) -> impl IntoResponse {
    let map = db.read().await;
    if let Some(task) = map.get(&id) {
        (StatusCode::OK, Json(task.clone())).into_response()
    } else {
        (StatusCode::NOT_FOUND, Json(serde_json::json!({"error": "task not found"}))).into_response()
    }
}

// Handler:建立任務
async fn create_task(
    Extension(db): Extension<Db>,
    Extension(id_counter): Extension<IdCounter>,
    Json(payload): Json<CreateTask>,
) -> impl IntoResponse {
    let id = id_counter.fetch_add(1, Ordering::Relaxed);
    let task = Task {
        id,
        title: payload.title,
        description: payload.description,
        completed: false,
    };

    let mut map = db.write().await;
    map.insert(id, task.clone());

    (StatusCode::CREATED, Json(task))
}

// Handler:更新任務(部分更新)
async fn update_task(
    Path(id): Path<u64>,
    Extension(db): Extension<Db>,
    Json(payload): Json<UpdateTask>,
) -> impl IntoResponse {
    let mut map = db.write().await;
    if let Some(task) = map.get_mut(&id) {
        if let Some(title) = payload.title { task.title = title; }
        if let Some(desc) = payload.description { task.description = Some(desc); }
        if let Some(comp) = payload.completed { task.completed = comp; }
        return (StatusCode::OK, Json(task.clone())).into_response();
    }

    (StatusCode::NOT_FOUND, Json(serde_json::json!({"error": "task not found"}))).into_response()
}

// Handler:刪除任務
async fn delete_task(Path(id): Path<u64>, Extension(db): Extension<Db>) -> impl IntoResponse {
    let mut map = db.write().await;
    if map.remove(&id).is_some() {
        (StatusCode::NO_CONTENT, Json(serde_json::json!({}))).into_response()
    } else {
        (StatusCode::NOT_FOUND, Json(serde_json::json!({"error": "task not found"}))).into_response()
    }
}

如何在你自己的電腦完成專案

  1. 建專案:cargo new axum_restful_api
  2. 把上面的 Cargo.toml 依賴加入(或直接修改新專案的 Cargo.toml)
  3. 把 main.rs 放到 src/main.rs
  4. 編譯並執行:cargo run
  5. 用 curl 或 Postman 測試 API

範例 API 請求與回應

為什麼這樣設計

  • Extension 注入共享狀態:Axum 提供 Extension 作為簡單方式把共享資源注入 handler,方便在 handler 裡直接取得 Db。把 db 與 id_counter 都用 Extension 傳入,可以讓 handler 不必使用全域變數。
  • RwLock vs Mutex:使用 RwLock 可以讓多個讀操作同時進行(list、get),而寫操作(create、update、delete)會取得寫鎖以確保一致性。若使用 Mutex,讀操作也會互斥,效能相對差一些。不過若程式簡單、流量低,Mutex 也足夠。這裡選用 tokio::sync::RwLock(非 std)以支援非同步鎖定。
  • AtomicU64 vs uuid:AtomicU64 產生簡單增量 id,方便閱讀與測試;uuid 則適合分散系統或避免衝突的情境。兩者各有用途。
  • 回傳 StatusCode:依 REST 慣例使用適當 HTTP 狀態碼(201 表示已建立,204 刪除成功但無回傳 body,404 未找到等),利於客戶端處理。
  • JSON schema:使用 serde 派生序列化 / 反序列化,允許 handler 接收 Json 提取器並自動轉型與驗證結構。

上一篇
Day 13 Axum 錯誤處理:自訂錯誤型別
下一篇
Day15 Rust 專案連接資料庫(PostgreSQL)
系列文
Rust 後端入門16
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言