Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

完整 Rust RESTful API 開發指南

📋 目錄

專案概述

這是一個使用 Rust 語言和 Axum 框架開發的完整 RESTful API 專案。專案展示瞭如何使用現代 Rust 技術棧構建高性能、類型安全的 web 服務。

技術棧

  • 語言: Rust 1.70+
  • 框架: Axum 0.7
  • 異步運行時: Tokio
  • 序列化: Serde
  • 日誌: Tracing
  • 測試: 內建測試框架

主要功能

  • 用戶管理(CRUD 操作)
  • 輸入驗證和錯誤處理
  • 結構化日誌記錄
  • 健康檢查端點
  • 完整的測試覆蓋

環境準備

系統要求

  • Rust 1.70 或更高版本
  • Cargo 包管理器
  • curl 或 HTTPie(用於測試)

安裝 Rust

# 安裝 Rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

# 更新到最新版本
rustup update

# 驗證安裝
rustc --version
cargo --version

創建專案

# 創建新專案
cargo new rust-restful-api
cd rust-restful-api

專案結構

rust-restful-api/
├── Cargo.toml              # 依賴配置
├── src/
│   └── main.rs            # 主程式
├── tests/                 # 測試檔案
│   └── integration_tests.rs
├── scripts/               # 腳本檔案
│   └── test_api.sh
├── docker/                # Docker 配置
│   └── Dockerfile
└── README.md              # 專案說明

核心代碼

1. Cargo.toml 配置

[package]
name = "rust-restful-api"
version = "0.1.0"
edition = "2021"
description = "A complete RESTful API example in Rust using Axum"

[dependencies]
axum = "0.7"
tokio = { version = "1.0", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
uuid = { version = "1.0", features = ["v4", "serde"] }
tracing = "0.1"
tracing-subscriber = "0.3"
anyhow = "1.0"
chrono = { version = "0.4", features = ["serde"] }
tower = "0.4"

[dev-dependencies]
tower-test = "0.4"

2. 主程式 (src/main.rs)

use axum::{
    extract::{Json, Path, State},
    http::StatusCode,
    response::Json as ResponseJson,
    routing::{delete, get, post, put},
    Router,
};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use tokio::net::TcpListener;
use tracing::{info, warn, Level};
use uuid::Uuid;

// 數據模型
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct User {
    pub id: Uuid,
    pub name: String,
    pub email: String,
    pub age: u32,
    pub created_at: DateTime<Utc>,
    pub updated_at: DateTime<Utc>,
}

#[derive(Debug, Deserialize)]
pub struct CreateUserRequest {
    pub name: String,
    pub email: String,
    pub age: u32,
}

#[derive(Debug, Deserialize)]
pub struct UpdateUserRequest {
    pub name: Option<String>,
    pub email: Option<String>,
    pub age: Option<u32>,
}

// 響應模型
#[derive(Debug, Serialize)]
pub struct UserResponse {
    pub id: Uuid,
    pub name: String,
    pub email: String,
    pub age: u32,
    pub created_at: DateTime<Utc>,
    pub updated_at: DateTime<Utc>,
}

#[derive(Debug, Serialize)]
pub struct UsersListResponse {
    pub users: Vec<UserResponse>,
    pub total: usize,
}

#[derive(Debug, Serialize)]
pub struct ErrorResponse {
    pub error: String,
    pub code: u16,
    pub timestamp: DateTime<Utc>,
}

#[derive(Debug, Serialize)]
pub struct HealthResponse {
    pub status: String,
    pub timestamp: DateTime<Utc>,
    pub version: String,
}

// 應用狀態
pub type AppState = Arc<Mutex<HashMap<Uuid, User>>>;

// 輔助函數
impl User {
    fn new(name: String, email: String, age: u32) -> Self {
        let now = Utc::now();
        Self {
            id: Uuid::new_v4(),
            name,
            email,
            age,
            created_at: now,
            updated_at: now,
        }
    }
    
    fn update(&mut self, request: UpdateUserRequest) {
        if let Some(name) = request.name {
            self.name = name;
        }
        if let Some(email) = request.email {
            self.email = email;
        }
        if let Some(age) = request.age {
            self.age = age;
        }
        self.updated_at = Utc::now();
    }
}

impl From<User> for UserResponse {
    fn from(user: User) -> Self {
        Self {
            id: user.id,
            name: user.name,
            email: user.email,
            age: user.age,
            created_at: user.created_at,
            updated_at: user.updated_at,
        }
    }
}

// 錯誤處理
fn create_error_response(error: &str, code: u16) -> ErrorResponse {
    ErrorResponse {
        error: error.to_string(),
        code,
        timestamp: Utc::now(),
    }
}

// 輸入驗證
fn validate_create_user_request(request: &CreateUserRequest) -> Result<(), String> {
    if request.name.trim().is_empty() {
        return Err("Name cannot be empty".to_string());
    }
    if request.email.trim().is_empty() {
        return Err("Email cannot be empty".to_string());
    }
    if !request.email.contains('@') {
        return Err("Invalid email format".to_string());
    }
    if request.age > 150 {
        return Err("Age must be reasonable".to_string());
    }
    Ok(())
}

// API 處理器
pub async fn health_check() -> ResponseJson<HealthResponse> {
    ResponseJson(HealthResponse {
        status: "healthy".to_string(),
        timestamp: Utc::now(),
        version: env!("CARGO_PKG_VERSION").to_string(),
    })
}

pub async fn get_all_users(
    State(state): State<AppState>
) -> Result<ResponseJson<UsersListResponse>, (StatusCode, ResponseJson<ErrorResponse>)> {
    let users = state.lock().unwrap();
    let user_list: Vec<UserResponse> = users.values()
        .cloned()
        .map(UserResponse::from)
        .collect();
    
    let total = user_list.len();
    
    Ok(ResponseJson(UsersListResponse {
        users: user_list,
        total,
    }))
}

pub async fn get_user(
    State(state): State<AppState>,
    Path(id): Path<Uuid>,
) -> Result<ResponseJson<UserResponse>, (StatusCode, ResponseJson<ErrorResponse>)> {
    let users = state.lock().unwrap();
    
    match users.get(&id) {
        Some(user) => {
            info!("Retrieved user: {}", id);
            Ok(ResponseJson(UserResponse::from(user.clone())))
        }
        None => {
            warn!("User not found: {}", id);
            Err((
                StatusCode::NOT_FOUND,
                ResponseJson(create_error_response("User not found", 404)),
            ))
        }
    }
}

pub async fn create_user(
    State(state): State<AppState>,
    Json(payload): Json<CreateUserRequest>,
) -> Result<(StatusCode, ResponseJson<UserResponse>), (StatusCode, ResponseJson<ErrorResponse>)> {
    // 驗證輸入
    if let Err(error) = validate_create_user_request(&payload) {
        warn!("Invalid user creation request: {}", error);
        return Err((
            StatusCode::BAD_REQUEST,
            ResponseJson(create_error_response(&error, 400)),
        ));
    }

    // 檢查郵箱是否已存在
    {
        let users = state.lock().unwrap();
        if users.values().any(|u| u.email == payload.email) {
            return Err((
                StatusCode::CONFLICT,
                ResponseJson(create_error_response("Email already exists", 409)),
            ));
        }
    }

    let user = User::new(payload.name, payload.email, payload.age);
    let user_id = user.id;

    let mut users = state.lock().unwrap();
    users.insert(user_id, user.clone());

    info!("Created user: {} ({})", user.name, user_id);
    Ok((StatusCode::CREATED, ResponseJson(UserResponse::from(user))))
}

pub async fn update_user(
    State(state): State<AppState>,
    Path(id): Path<Uuid>,
    Json(payload): Json<UpdateUserRequest>,
) -> Result<ResponseJson<UserResponse>, (StatusCode, ResponseJson<ErrorResponse>)> {
    // 檢查郵箱衝突(如果更新郵箱)
    if let Some(ref new_email) = payload.email {
        let users = state.lock().unwrap();
        if users.values().any(|u| u.id != id && u.email == *new_email) {
            return Err((
                StatusCode::CONFLICT,
                ResponseJson(create_error_response("Email already exists", 409)),
            ));
        }
    }
    
    let mut users = state.lock().unwrap();
    match users.get_mut(&id) {
        Some(user) => {
            user.update(payload);
            info!("Updated user: {}", id);
            Ok(ResponseJson(UserResponse::from(user.clone())))
        }
        None => {
            warn!("Attempted to update non-existent user: {}", id);
            Err((
                StatusCode::NOT_FOUND,
                ResponseJson(create_error_response("User not found", 404)),
            ))
        }
    }
}

pub async fn delete_user(
    State(state): State<AppState>,
    Path(id): Path<Uuid>,
) -> Result<StatusCode, (StatusCode, ResponseJson<ErrorResponse>)> {
    let mut users = state.lock().unwrap();
    
    match users.remove(&id) {
        Some(user) => {
            info!("Deleted user: {} ({})", user.name, id);
            Ok(StatusCode::NO_CONTENT)
        }
        None => {
            warn!("Attempted to delete non-existent user: {}", id);
            Err((
                StatusCode::NOT_FOUND,
                ResponseJson(create_error_response("User not found", 404)),
            ))
        }
    }
}

// 路由構建
pub fn create_router() -> Router {
    let state = Arc::new(Mutex::new(HashMap::new()));

    Router::new()
        .route("/health", get(health_check))
        .route("/api/users", get(get_all_users))
        .route("/api/users", post(create_user))
        .route("/api/users/:id", get(get_user))
        .route("/api/users/:id", put(update_user))
        .route("/api/users/:id", delete(delete_user))
        .with_state(state)
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    // 初始化日誌
    tracing_subscriber::fmt()
        .with_max_level(Level::INFO)
        .init();

    // 創建應用
    let app = create_router();

    // 啟動服務器
    let port = std::env::var("PORT").unwrap_or_else(|_| "3000".to_string());
    let addr = format!("0.0.0.0:{}", port);
    
    info!("🚀 Starting server on {}", addr);
    info!("📚 Health check: http://localhost:{}/health", port);
    
    let listener = TcpListener::bind(&addr).await?;
    axum::serve(listener, app).await?;

    Ok(())
}

// 測試模組
#[cfg(test)]
mod tests {
    use super::*;
    use axum::http::{Method, Request};
    use axum::body::Body;
    use tower::ServiceExt;

    #[tokio::test]
    async fn test_health_check() {
        let app = create_router();
        
        let response = app
            .oneshot(
                Request::builder()
                    .method(Method::GET)
                    .uri("/health")
                    .body(Body::empty())
                    .unwrap(),
            )
            .await
            .unwrap();

        assert_eq!(response.status(), StatusCode::OK);
    }

    #[tokio::test]
    async fn test_create_user() {
        let app = create_router();
        
        let user_data = serde_json::json!({
            "name": "John Doe",
            "email": "john@example.com",
            "age": 30
        });

        let response = app
            .oneshot(
                Request::builder()
                    .method(Method::POST)
                    .uri("/api/users")
                    .header("content-type", "application/json")
                    .body(Body::from(user_data.to_string()))
                    .unwrap(),
            )
            .await
            .unwrap();

        assert_eq!(response.status(), StatusCode::CREATED);
    }

    #[tokio::test]
    async fn test_create_user_invalid_email() {
        let app = create_router();
        
        let user_data = serde_json::json!({
            "name": "John Doe",
            "email": "invalid-email",
            "age": 30
        });

        let response = app
            .oneshot(
                Request::builder()
                    .method(Method::POST)
                    .uri("/api/users")
                    .header("content-type", "application/json")
                    .body(Body::from(user_data.to_string()))
                    .unwrap(),
            )
            .await
            .unwrap();

        assert_eq!(response.status(), StatusCode::BAD_REQUEST);
    }

    #[tokio::test]
    async fn test_get_nonexistent_user() {
        let app = create_router();
        
        let response = app
            .oneshot(
                Request::builder()
                    .method(Method::GET)
                    .uri("/api/users/00000000-0000-0000-0000-000000000000")
                    .body(Body::empty())
                    .unwrap(),
            )
            .await
            .unwrap();

        assert_eq!(response.status(), StatusCode::NOT_FOUND);
    }

    #[tokio::test]
    async fn test_email_uniqueness() {
        let app = create_router();
        
        // 創建第一個用戶
        let user1_data = serde_json::json!({
            "name": "User One",
            "email": "duplicate@example.com",
            "age": 30
        });
        
        let response1 = app
            .clone()
            .oneshot(
                Request::builder()
                    .method(Method::POST)
                    .uri("/api/users")
                    .header("content-type", "application/json")
                    .body(Body::from(user1_data.to_string()))
                    .unwrap(),
            )
            .await
            .unwrap();
        
        assert_eq!(response1.status(), StatusCode::CREATED);
        
        // 嘗試創建具有相同電子郵件的第二個用戶
        let user2_data = serde_json::json!({
            "name": "User Two",
            "email": "duplicate@example.com",
            "age": 25
        });
        
        let response2 = app
            .oneshot(
                Request::builder()
                    .method(Method::POST)
                    .uri("/api/users")
                    .header("content-type", "application/json")
                    .body(Body::from(user2_data.to_string()))
                    .unwrap(),
            )
            .await
            .unwrap();
        
        assert_eq!(response2.status(), StatusCode::CONFLICT);
    }
}

功能特色

1. 🔒 類型安全

  • 強類型系統: 使用 Rust 的類型系統確保編譯時安全
  • 序列化保證: Serde 自動處理 JSON 序列化/反序列化
  • UUID 支持: 唯一標識符保證資料完整性

2. ⚡ 高性能

  • 異步處理: 基於 Tokio 的異步運行時
  • 零成本抽象: Axum 框架提供高效的請求處理
  • 內存安全: 無垃圾回收,低延遲響應

3. 🛡️ 錯誤處理

  • 統一錯誤格式: 所有錯誤響應都包含錯誤碼和時間戳
  • 輸入驗證: 全面的請求數據驗證
  • 適當的 HTTP 狀態碼: 符合 REST 標準的響應碼

4. 📊 日誌記錄

  • 結構化日誌: 使用 tracing 記錄詳細的操作信息
  • 可配置級別: 支持不同的日誌級別
  • 性能追蹤: 異步友好的日誌記錄

5. 🧪 測試覆蓋

  • 單元測試: 涵蓋所有主要功能
  • 集成測試: 測試完整的 API 流程
  • 邊界情況: 包含錯誤情況的測試

API 文檔

基本信息

  • Base URL: http://localhost:3000
  • Content-Type: application/json
  • 認證: 無(示例項目)

端點列表

1. 健康檢查

GET /health

響應示例:

{
  "status": "healthy",
  "timestamp": "2025-07-17T10:30:00.000Z",
  "version": "0.1.0"
}

2. 獲取所有用戶

GET /api/users

響應示例:

{
  "users": [
    {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "name": "張三",
      "email": "zhangsan@example.com",
      "age": 25,
      "created_at": "2025-07-17T10:30:00.000Z",
      "updated_at": "2025-07-17T10:30:00.000Z"
    }
  ],
  "total": 1
}

3. 創建用戶

POST /api/users

請求體:

{
  "name": "張三",
  "email": "zhangsan@example.com",
  "age": 25
}

響應示例 (201 Created):

{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "name": "張三",
  "email": "zhangsan@example.com",
  "age": 25,
  "created_at": "2025-07-17T10:30:00.000Z",
  "updated_at": "2025-07-17T10:30:00.000Z"
}

4. 獲取單個用戶

GET /api/users/{id}

路徑參數:

  • id: 用戶 UUID

響應示例:

{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "name": "張三",
  "email": "zhangsan@example.com",
  "age": 25,
  "created_at": "2025-07-17T10:30:00.000Z",
  "updated_at": "2025-07-17T10:30:00.000Z"
}

5. 更新用戶

PUT /api/users/{id}

請求體 (所有字段都是可選的):

{
  "name": "李四",
  "email": "lisi@example.com",
  "age": 30
}

響應示例:

{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "name": "李四",
  "email": "lisi@example.com",
  "age": 30,
  "created_at": "2025-07-17T10:30:00.000Z",
  "updated_at": "2025-07-17T11:00:00.000Z"
}

6. 刪除用戶

DELETE /api/users/{id}

響應: 204 No Content

錯誤響應格式

所有錯誤響應都遵循以下格式:

{
  "error": "錯誤訊息",
  "code": 404,
  "timestamp": "2025-07-17T10:30:00.000Z"
}

HTTP 狀態碼

狀態碼說明
200OK - 成功獲取資源
201Created - 成功創建資源
204No Content - 成功刪除資源
400Bad Request - 請求格式錯誤
404Not Found - 資源不存在
409Conflict - 資源衝突(如重複郵箱)
500Internal Server Error - 服務器內部錯誤

測試指南

運行項目

# 編譯並運行
cargo run

# 在背景運行
cargo run &

# 指定端口
PORT=8080 cargo run

單元測試

# 運行所有測試
cargo test

# 運行特定測試
cargo test test_health_check

# 顯示測試輸出
cargo test -- --nocapture

# 運行測試並顯示覆蓋率
cargo test --verbose

手動測試

使用 curl

# 1. 健康檢查
curl http://localhost:3000/health

# 2. 創建用戶
curl -X POST http://localhost:3000/api/users \
  -H "Content-Type: application/json" \
  -d '{
    "name": "張三",
    "email": "zhangsan@example.com",
    "age": 25
  }'

# 3. 獲取所有用戶
curl http://localhost:3000/api/users

# 4. 獲取單個用戶 (替換 UUID)
curl http://localhost:3000/api/users/550e8400-e29b-41d4-a716-446655440000

# 5. 更新用戶
curl -X PUT http://localhost:3000/api/users/550e8400-e29b-41d4-a716-446655440000 \
  -H "Content-Type: application/json" \
  -d '{
    "name": "李四",
    "age": 30
  }'

# 6. 刪除用戶
curl -X DELETE http://localhost:3000/api/users/550e8400-e29b-41d4-a716-446655440000

使用 HTTPie

# 安裝 HTTPie
pip install httpie

# 創建用戶
http POST localhost:3000/api/users name="王五" email="wangwu@example.com" age:=28

# 獲取用戶
http GET localhost:3000/api/users

# 更新用戶
http PUT localhost:3000/api/users/USER_ID name="趙六"

# 刪除用戶
http DELETE localhost:3000/api/users/USER_ID

自動化測試腳本

創建 scripts/test_api.sh:

#!/bin/bash

# 顏色定義
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'

BASE_URL="http://localhost:3000"
PASSED=0
FAILED=0

# 測試函數
test_endpoint() {
    local test_name="$1"
    local expected_code="$2"
    local actual_code="$3"
    
    echo -e "\n🧪 測試: $test_name"
    echo "期望狀態碼: $expected_code"
    echo "實際狀態碼: $actual_code"
    
    if [ "$actual_code" = "$expected_code" ]; then
        echo -e "${GREEN}✅ 通過${NC}"
        ((PASSED++))
    else
        echo -e "${RED}❌ 失敗${NC}"
        ((FAILED++))
    fi
}

echo -e "${YELLOW}🚀 開始測試 Rust RESTful API${NC}"

# 檢查服務器
if ! curl -s "$BASE_URL/health" > /dev/null; then
    echo -e "${RED}❌ 服務器未運行${NC}"
    exit 1
fi

# 1. 健康檢查
echo -e "\n1️⃣ 健康檢查"
response=$(curl -s -w "%{http_code}" -o /tmp/health_response "$BASE_URL/health")
status_code="${response: -3}"
test_endpoint "健康檢查" "200" "$status_code"

# 2. 創建用戶
echo -e "\n2️⃣ 創建用戶"
response=$(curl -s -w "%{http_code}" -o /tmp/create_response \
    -X POST "$BASE_URL/api/users" \
    -H "Content-Type: application/json" \
    -d '{"name":"張三","email":"zhangsan@example.com","age":25}')
status_code="${response: -3}"
test_endpoint "創建用戶" "201" "$status_code"

# 提取用戶 ID
USER_ID=$(cat /tmp/create_response | grep -o '"id":"[^"]*"' | cut -d'"' -f4)
echo -e "${YELLOW}用戶 ID: $USER_ID${NC}"

# 3. 獲取所有用戶
echo -e "\n3️⃣ 獲取所有用戶"
response=$(curl -s -w "%{http_code}" -o /tmp/users_response "$BASE_URL/api/users")
status_code="${response: -3}"
test_endpoint "獲取所有用戶" "200" "$status_code"

# 4. 獲取單個用戶
if [ -n "$USER_ID" ]; then
    echo -e "\n4️⃣ 獲取單個用戶"
    response=$(curl -s -w "%{http_code}" -o /tmp/user_response "$BASE_URL/api/users/$USER_ID")
    status_code="${response: -3}"
    test_endpoint "獲取單個用戶" "200" "$status_code"
    
    # 5. 更新用戶
    echo -e "\n5️⃣ 更新用戶"
    response=$(curl -s -w "%{http_code}" -o /tmp/update_response \
        -X PUT "$BASE_URL/api/users/$USER_ID" \
        -H "Content-Type: application/json" \
        -d '{"name":"李四","age":30}')
    status_code="${response: -3}"
    test_endpoint "更新用戶" "200" "$status_code"
    
    # 6. 刪除用戶
    echo -e "\n6️⃣ 刪除用戶"
    response=$(curl -s -w "%{http_code}" -o /tmp/delete_response \
        -X DELETE "$BASE_URL/api/users/$USER_ID")
    status_code="${response: -3}"
    test_endpoint "刪除用戶" "204" "$status_code"