Rust

第十九章:实战 —— 用 Axum 构建 Web API

第十九章:实战 —— 用 Axum 构建 Web API

本章目标

  • 理解 Axum 框架的设计理念(对比 Express.js)
  • 掌握路由定义与处理器函数
  • 熟练使用请求解析:Path、Query、Json
  • 实现中间件(日志、认证、CORS)
  • 集成 SQLx 进行数据库操作
  • 构建优雅的错误处理体系
  • 从零搭建一个完整的 REST API 项目(Todo 应用)
  • 编写 API 测试
  • 部署到生产环境

预计学习时间:150 - 180 分钟(这是一个综合性实战章节)


19.1 Axum 框架介绍

19.1.1 为什么选择 Axum?

┌──────────────────────────────────────────────────────────────────┐
│                  Rust Web 框架对比                                │
├──────────────┬──────────────┬────────────────┬──────────────────┤
│   特性        │    Axum      │    Actix-web   │    Rocket        │
├──────────────┼──────────────┼────────────────┼──────────────────┤
│  维护者       │  Tokio 团队   │  社区          │  社区            │
│  异步运行时   │  Tokio       │  Actix/Tokio   │  Tokio           │
│  类型安全     │  ⭐⭐⭐      │  ⭐⭐          │  ⭐⭐⭐          │
│  学习曲线     │  中等         │  中等          │  较低            │
│  性能         │  极高         │  极高          │  高              │
│  生态         │  Tower 生态   │  自有生态      │  自有生态         │
│  宏依赖       │  少           │  多            │  多              │
│  GitHub Stars │  19k+         │  21k+          │  24k+            │
│  类比 JS      │  Koa/Fastify  │  Express       │  NestJS          │
└──────────────┴──────────────┴────────────────┴──────────────────┘

选择 Axum 的理由:
1. 由 Tokio 团队维护,与 Tokio 生态无缝集成
2. 基于 Tower 中间件系统,复用性强
3. 最少的宏魔法,代码就是普通的 Rust 函数
4. 编译时错误信息清晰
5. 社区增长最快

19.1.2 对比 Express.js

// Express.js - 最流行的 Node.js Web 框架
const express = require('express');
const app = express();

app.use(express.json());

app.get('/users', (req, res) => {
    res.json([{ id: 1, name: 'Alice' }]);
});

app.get('/users/:id', (req, res) => {
    const id = parseInt(req.params.id);
    res.json({ id, name: 'Alice' });
});

app.post('/users', (req, res) => {
    const { name, email } = req.body;
    res.status(201).json({ id: 1, name, email });
});

app.listen(3000, () => {
    console.log('Server running on port 3000');
});
// Axum - 等价的 Rust 实现
use axum::{
    routing::{get, post},
    extract::{Path, Json},
    Router,
    http::StatusCode,
};
use serde::{Deserialize, Serialize};

#[derive(Serialize)]
struct User {
    id: u64,
    name: String,
}

#[derive(Deserialize)]
struct CreateUser {
    name: String,
    email: String,
}

// 处理器就是普通的 async 函数!
async fn list_users() -> Json<Vec<User>> {
    Json(vec![User { id: 1, name: "Alice".to_string() }])
}

async fn get_user(Path(id): Path<u64>) -> Json<User> {
    Json(User { id, name: "Alice".to_string() })
}

async fn create_user(
    Json(payload): Json<CreateUser>,
) -> (StatusCode, Json<User>) {
    let user = User { id: 1, name: payload.name };
    (StatusCode::CREATED, Json(user))
}

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

    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
    println!("服务器运行在 http://localhost:3000");
    axum::serve(listener, app).await.unwrap();
}

19.1.3 关键区别

┌──────────────────────────────────────────────────────────────┐
│              Express.js vs Axum 关键区别                      │
├──────────────────┬───────────────────┬───────────────────────┤
│  方面             │  Express.js       │  Axum                 │
├──────────────────┼───────────────────┼───────────────────────┤
│  请求处理         │  回调函数          │  async 函数           │
│  类型安全         │  运行时检查        │  编译时检查           │
│  JSON 解析        │  中间件处理        │  提取器自动解析       │
│  错误处理         │  next(err)        │  Result<T, E>        │
│  路由参数         │  req.params.id    │  Path(id): Path<u64> │
│  查询参数         │  req.query.page   │  Query(q): Query<Q>  │
│  请求体           │  req.body         │  Json(body): Json<T> │
│  中间件           │  app.use(fn)      │  layer(middleware)   │
│  性能             │  ~15K req/s       │  ~200K req/s         │
│  内存             │  ~50MB            │  ~5MB                │
└──────────────────┴───────────────────┴───────────────────────┘

19.2 项目初始化

19.2.1 创建项目

cargo new todo-api
cd todo-api

19.2.2 依赖配置

# Cargo.toml
[package]
name = "todo-api"
version = "0.1.0"
edition = "2021"

[dependencies]
# Web 框架
axum = "0.8"
tokio = { version = "1", features = ["full"] }

# 序列化/反序列化
serde = { version = "1", features = ["derive"] }
serde_json = "1"

# 数据库
sqlx = { version = "0.8", features = ["runtime-tokio", "sqlite", "chrono"] }

# 错误处理
anyhow = "1"
thiserror = "2"

# 日志
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }

# 中间件
tower = "0.5"
tower-http = { version = "0.6", features = ["cors", "trace", "timeout"] }

# 时间处理
chrono = { version = "0.4", features = ["serde"] }

# 环境变量
dotenvy = "0.15"

# UUID
uuid = { version = "1", features = ["v4", "serde"] }

19.2.3 项目结构

todo-api/
├── Cargo.toml
├── .env                    # 环境变量
├── migrations/             # 数据库迁移
│   └── 001_create_todos.sql
├── src/
│   ├── main.rs            # 入口
│   ├── config.rs          # 配置
│   ├── routes/            # 路由
│   │   ├── mod.rs
│   │   └── todos.rs
│   ├── models/            # 数据模型
│   │   ├── mod.rs
│   │   └── todo.rs
│   ├── handlers/          # 请求处理器
│   │   ├── mod.rs
│   │   └── todos.rs
│   ├── error.rs           # 错误处理
│   └── db.rs              # 数据库连接
└── tests/
    └── api_tests.rs       # API 测试

19.3 路由与处理器

19.3.1 路由定义

use axum::{
    routing::{get, post, put, delete},
    Router,
};

// Axum 的路由系统非常直观
fn create_router() -> Router {
    Router::new()
        // 基本路由
        .route("/", get(root_handler))

        // RESTful 路由 —— 同一路径不同方法
        .route("/todos", get(list_todos).post(create_todo))
        .route("/todos/{id}", get(get_todo).put(update_todo).delete(delete_todo))

        // 嵌套路由
        .nest("/api/v1", api_v1_routes())
        .nest("/api/v2", api_v2_routes())

        // 合并多个 Router
        .merge(health_routes())
}

// 对比 Express.js:
// app.get('/', rootHandler);
// app.route('/todos')
//     .get(listTodos)
//     .post(createTodo);
// app.route('/todos/:id')
//     .get(getTodo)
//     .put(updateTodo)
//     .delete(deleteTodo);
// app.use('/api/v1', apiV1Router);

fn api_v1_routes() -> Router {
    Router::new()
        .route("/users", get(|| async { "v1 users" }))
}

fn api_v2_routes() -> Router {
    Router::new()
        .route("/users", get(|| async { "v2 users" }))
}

fn health_routes() -> Router {
    Router::new()
        .route("/health", get(|| async { "OK" }))
        .route("/ready", get(|| async { "Ready" }))
}

async fn root_handler() -> &'static str {
    "欢迎使用 Todo API!"
}

// 占位处理器
async fn list_todos() -> &'static str { "list" }
async fn create_todo() -> &'static str { "create" }
async fn get_todo() -> &'static str { "get" }
async fn update_todo() -> &'static str { "update" }
async fn delete_todo() -> &'static str { "delete" }

19.3.2 处理器函数

在 Axum 中,处理器就是普通的 async 函数。函数的参数是”提取器”(Extractors),返回值是”响应”:

use axum::{
    extract::{Path, Query, Json, State},
    http::StatusCode,
    response::IntoResponse,
};
use serde::{Deserialize, Serialize};

// === 无参数处理器 ===
async fn hello() -> &'static str {
    "Hello, World!"
}

// === 返回 JSON ===
#[derive(Serialize)]
struct Message {
    text: String,
    timestamp: i64,
}

async fn json_hello() -> Json<Message> {
    Json(Message {
        text: "Hello!".to_string(),
        timestamp: chrono::Utc::now().timestamp(),
    })
}

// === 返回状态码 + JSON ===
async fn created_response() -> (StatusCode, Json<Message>) {
    (
        StatusCode::CREATED,
        Json(Message {
            text: "已创建".to_string(),
            timestamp: chrono::Utc::now().timestamp(),
        }),
    )
}

// === 返回自定义响应头 ===
use axum::http::header;
use axum::response::Response;

async fn custom_headers() -> impl IntoResponse {
    (
        StatusCode::OK,
        [(header::CONTENT_TYPE, "text/plain; charset=utf-8")],
        "带自定义头的响应",
    )
}

19.4 请求解析(提取器)

19.4.1 Path —— 路径参数

use axum::extract::Path;

// 单个路径参数
// GET /users/42
async fn get_user(Path(id): Path<u64>) -> String {
    format!("用户 ID: {}", id)
}

// 多个路径参数
// GET /users/42/posts/7
async fn get_user_post(
    Path((user_id, post_id)): Path<(u64, u64)>,
) -> String {
    format!("用户 {} 的帖子 {}", user_id, post_id)
}

// 使用结构体解构路径参数
#[derive(Deserialize)]
struct UserPostPath {
    user_id: u64,
    post_id: u64,
}

async fn get_user_post_v2(
    Path(path): Path<UserPostPath>,
) -> String {
    format!("用户 {} 的帖子 {}", path.user_id, path.post_id)
}

// 路由定义:
// .route("/users/{user_id}/posts/{post_id}", get(get_user_post_v2))

// 对比 Express.js:
// app.get('/users/:userId/posts/:postId', (req, res) => {
//     const { userId, postId } = req.params;
//     // userId 是 string!需要手动转换
//     const id = parseInt(userId);  // 可能是 NaN!
// });
// Axum 的优势:编译时检查类型,Path<u64> 保证是有效数字!

19.4.2 Query —— 查询参数

use axum::extract::Query;
use serde::Deserialize;

// GET /todos?page=1&limit=10&status=active
#[derive(Deserialize, Debug)]
struct TodoQuery {
    page: Option<u32>,      // 可选参数
    limit: Option<u32>,     // 可选参数
    status: Option<String>, // 可选参数
    #[serde(default)]       // 默认值:false
    completed: bool,
}

async fn list_todos(Query(query): Query<TodoQuery>) -> String {
    let page = query.page.unwrap_or(1);
    let limit = query.limit.unwrap_or(20);
    format!(
        "查询:page={}, limit={}, status={:?}, completed={}",
        page, limit, query.status, query.completed
    )
}

// 对比 Express.js:
// app.get('/todos', (req, res) => {
//     const page = parseInt(req.query.page) || 1;  // 可能 NaN
//     const limit = parseInt(req.query.limit) || 20;
//     const status = req.query.status;  // undefined 或 string
// });
// Axum 自动解析、验证类型,无效参数直接返回 400 错误!

19.4.3 Json —— 请求体

use axum::extract::Json;
use serde::{Deserialize, Serialize};

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

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

// POST /todos
// Body: { "title": "学习 Rust", "description": "完成第19章" }
async fn create_todo(
    Json(payload): Json<CreateTodo>,
) -> (StatusCode, Json<Todo>) {
    let todo = Todo {
        id: 1,
        title: payload.title,
        description: payload.description,
        completed: false,
    };
    (StatusCode::CREATED, Json(todo))
}

// 对比 Express.js:
// app.post('/todos', (req, res) => {
//     const { title, description } = req.body;
//     // title 可能是 undefined、null、number、array...
//     // 需要手动验证每个字段!
//     if (!title || typeof title !== 'string') {
//         return res.status(400).json({ error: 'title 必须是字符串' });
//     }
// });
// Axum + serde 自动验证:
// - title 必须存在(非 Option)
// - title 必须是字符串
// - description 可以不存在(Option)
// - 多余字段默认被忽略
// - 类型不对自动返回 400!

19.4.4 State —— 共享状态

use axum::extract::State;
use std::sync::Arc;
use tokio::sync::RwLock;

// 应用状态
#[derive(Clone)]
struct AppState {
    db: sqlx::SqlitePool,
    config: AppConfig,
}

#[derive(Clone)]
struct AppConfig {
    max_todos: usize,
    app_name: String,
}

// 在处理器中访问状态
async fn list_todos(State(state): State<AppState>) -> String {
    format!("应用名称:{}", state.config.app_name)
}

// 设置路由时注入状态
fn create_router(state: AppState) -> Router {
    Router::new()
        .route("/todos", get(list_todos))
        .with_state(state)  // 注入状态
}

// 对比 Express.js:
// // Express 通常用中间件或 app.locals
// app.locals.db = db;
// app.use((req, res, next) => {
//     req.db = app.locals.db;
//     next();
// });

// 使用 Arc + RwLock 共享可变状态
type SharedState = Arc<RwLock<Vec<Todo>>>;

#[derive(Clone, Serialize)]
struct Todo {
    id: u64,
    title: String,
    completed: bool,
}

async fn list_todos_shared(
    State(todos): State<SharedState>,
) -> Json<Vec<Todo>> {
    let todos = todos.read().await;
    Json(todos.clone())
}

async fn add_todo_shared(
    State(todos): State<SharedState>,
    Json(payload): Json<CreateTodoRequest>,
) -> (StatusCode, Json<Todo>) {
    let mut todos = todos.write().await;
    let todo = Todo {
        id: todos.len() as u64 + 1,
        title: payload.title,
        completed: false,
    };
    todos.push(todo.clone());
    (StatusCode::CREATED, Json(todo))
}

#[derive(Deserialize)]
struct CreateTodoRequest {
    title: String,
}

19.4.5 Headers 和其他提取器

use axum::{
    extract::ConnectInfo,
    http::{HeaderMap, Method, Uri},
};
use std::net::SocketAddr;

// 提取请求头
async fn show_headers(headers: HeaderMap) -> String {
    let user_agent = headers
        .get("user-agent")
        .and_then(|v| v.to_str().ok())
        .unwrap_or("未知");
    format!("User-Agent: {}", user_agent)
}

// 提取请求方法和 URI
async fn request_info(method: Method, uri: Uri) -> String {
    format!("{} {}", method, uri)
}

// 提取客户端 IP
async fn client_ip(ConnectInfo(addr): ConnectInfo<SocketAddr>) -> String {
    format!("你的 IP: {}", addr)
}

// 多个提取器组合(注意顺序:Body 消耗型提取器必须放最后)
async fn complex_handler(
    State(state): State<AppState>,      // 状态
    headers: HeaderMap,                   // 请求头
    Path(id): Path<u64>,                  // 路径参数
    Query(query): Query<TodoQuery>,       // 查询参数
    Json(body): Json<CreateTodo>,         // 请求体(必须最后!)
) -> impl IntoResponse {
    // ... 处理逻辑
    StatusCode::OK
}

// ⚠️ 重要规则:
// Json、String、Bytes 等消耗请求体的提取器必须作为最后一个参数!
// 因为请求体只能被读取一次。

#[derive(Clone)]
struct AppState;
#[derive(Deserialize)]
struct TodoQuery { page: Option<u32> }
#[derive(Deserialize)]
struct CreateTodo { title: String }

19.5 中间件

19.5.1 Tower 中间件系统

use axum::{Router, middleware};
use tower_http::{
    cors::{CorsLayer, Any},
    trace::TraceLayer,
    timeout::TimeoutLayer,
};
use std::time::Duration;

fn create_router() -> Router {
    Router::new()
        .route("/todos", get(list_todos))
        // 添加中间件(从下到上执行)
        .layer(TimeoutLayer::new(Duration::from_secs(30)))   // 请求超时
        .layer(CorsLayer::new()                               // CORS
            .allow_origin(Any)
            .allow_methods(Any)
            .allow_headers(Any))
        .layer(TraceLayer::new_for_http())                    // 请求日志
}

// 对比 Express.js:
// app.use(cors());                    // CORS
// app.use(morgan('dev'));             // 请求日志
// app.use(timeout('30s'));            // 请求超时

async fn list_todos() -> &'static str { "todos" }

19.5.2 自定义中间件

use axum::{
    Router,
    middleware::{self, Next},
    extract::Request,
    response::Response,
    http::StatusCode,
};

// 方法 1:使用 from_fn 创建中间件(最简单)
async fn logging_middleware(
    request: Request,
    next: Next,
) -> Response {
    let method = request.method().clone();
    let uri = request.uri().clone();
    let start = std::time::Instant::now();

    // 打印请求信息
    tracing::info!("{} {}", method, uri);

    // 调用下一个处理器
    let response = next.run(request).await;

    // 打印响应信息
    let duration = start.elapsed();
    tracing::info!(
        "{} {} {} {:?}",
        method, uri,
        response.status(),
        duration
    );

    response
}

// 方法 2:认证中间件
async fn auth_middleware(
    headers: axum::http::HeaderMap,
    request: Request,
    next: Next,
) -> Result<Response, StatusCode> {
    // 检查 Authorization 头
    let auth_header = headers
        .get("authorization")
        .and_then(|v| v.to_str().ok());

    match auth_header {
        Some(token) if token.starts_with("Bearer ") => {
            let token = &token[7..];
            // 验证 token(简化示例)
            if token == "valid-token" {
                Ok(next.run(request).await)
            } else {
                Err(StatusCode::UNAUTHORIZED)
            }
        }
        _ => Err(StatusCode::UNAUTHORIZED),
    }
}

// 使用中间件
fn create_router() -> Router {
    // 公共路由(不需要认证)
    let public_routes = Router::new()
        .route("/health", axum::routing::get(|| async { "OK" }))
        .route("/login", axum::routing::post(login));

    // 受保护的路由(需要认证)
    let protected_routes = Router::new()
        .route("/todos", axum::routing::get(list_todos))
        .route("/profile", axum::routing::get(get_profile))
        .layer(middleware::from_fn(auth_middleware));

    Router::new()
        .merge(public_routes)
        .merge(protected_routes)
        .layer(middleware::from_fn(logging_middleware))  // 所有路由都有日志
}

async fn login() -> &'static str { "login" }
async fn list_todos() -> &'static str { "todos" }
async fn get_profile() -> &'static str { "profile" }

// 对比 Express.js:
// const authMiddleware = (req, res, next) => {
//     const token = req.headers.authorization?.replace('Bearer ', '');
//     if (!token || !isValidToken(token)) {
//         return res.status(401).json({ error: 'Unauthorized' });
//     }
//     next();
// };
// app.use('/api', authMiddleware);

19.5.3 请求 ID 中间件

use axum::{
    middleware::Next,
    extract::Request,
    response::Response,
    http::header::HeaderName,
};
use uuid::Uuid;

/// 为每个请求添加唯一 ID(方便日志追踪)
async fn request_id_middleware(
    mut request: Request,
    next: Next,
) -> Response {
    let request_id = Uuid::new_v4().to_string();

    // 将 request_id 添加到请求扩展中(供后续处理器使用)
    request.extensions_mut().insert(RequestId(request_id.clone()));

    let mut response = next.run(request).await;

    // 在响应头中添加 request_id
    let header_name = HeaderName::from_static("x-request-id");
    response.headers_mut().insert(
        header_name,
        request_id.parse().unwrap(),
    );

    response
}

#[derive(Clone)]
struct RequestId(String);

// 在处理器中使用 request_id
use axum::Extension;

async fn handler(Extension(request_id): Extension<RequestId>) -> String {
    format!("Request ID: {}", request_id.0)
}

19.6 数据库集成(SQLx)

19.6.1 为什么选择 SQLx?

┌──────────────────────────────────────────────────────────────┐
│                Rust ORM/数据库库对比                           │
├──────────────┬──────────────────┬────────────────────────────┤
│  库           │  类型            │  特点                      │
├──────────────┼──────────────────┼────────────────────────────┤
│  SQLx        │  SQL 查询构建器   │  编译时检查 SQL,异步原生    │
│  Diesel      │  ORM             │  类型安全,同步             │
│  SeaORM      │  ORM             │  基于 SQLx,异步            │
│  rusqlite    │  SQLite 绑定      │  轻量,同步                │
└──────────────┴──────────────────┴────────────────────────────┘

SQLx 的优势:
1. 编译时检查 SQL 语句(连接数据库检查)
2. 原生异步支持(与 Tokio 完美配合)
3. 不是 ORM,直接写 SQL,更灵活
4. 支持 PostgreSQL、MySQL、SQLite
5. 类似 JS 的 knex.js 但有编译时检查

19.6.2 数据库设置

# 创建 .env 文件
echo "DATABASE_URL=sqlite:todos.db" > .env

# 安装 SQLx CLI
cargo install sqlx-cli --features sqlite

# 创建数据库
sqlx database create

# 创建迁移
sqlx migrate add create_todos
-- migrations/20240101000000_create_todos.sql
-- 创建 todos 表

CREATE TABLE IF NOT EXISTS todos (
    id TEXT PRIMARY KEY NOT NULL,
    title TEXT NOT NULL,
    description TEXT,
    completed BOOLEAN NOT NULL DEFAULT FALSE,
    created_at TEXT NOT NULL DEFAULT (datetime('now')),
    updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);

-- 创建索引
CREATE INDEX IF NOT EXISTS idx_todos_completed ON todos(completed);
CREATE INDEX IF NOT EXISTS idx_todos_created_at ON todos(created_at);
# 运行迁移
sqlx migrate run

19.6.3 数据库连接

// src/db.rs
use sqlx::sqlite::SqlitePoolOptions;
use sqlx::SqlitePool;
use anyhow::Result;

/// 创建数据库连接池
pub async fn create_pool(database_url: &str) -> Result<SqlitePool> {
    let pool = SqlitePoolOptions::new()
        .max_connections(5)              // 最大连接数
        .min_connections(1)              // 最小连接数
        .acquire_timeout(std::time::Duration::from_secs(5))  // 获取连接超时
        .connect(database_url)
        .await?;

    // 运行迁移
    sqlx::migrate!("./migrations")
        .run(&pool)
        .await?;

    tracing::info!("数据库连接成功");
    Ok(pool)
}

// 对比 JavaScript (knex.js):
// const knex = require('knex')({
//     client: 'sqlite3',
//     connection: { filename: './todos.db' },
//     pool: { min: 1, max: 5 },
// });

19.6.4 数据模型

// src/models/todo.rs
use chrono::NaiveDateTime;
use serde::{Deserialize, Serialize};
use sqlx::FromRow;
use uuid::Uuid;

/// 数据库中的 Todo 记录
#[derive(Debug, Clone, Serialize, FromRow)]
pub struct Todo {
    pub id: String,
    pub title: String,
    pub description: Option<String>,
    pub completed: bool,
    pub created_at: String,
    pub updated_at: String,
}

/// 创建 Todo 的请求
#[derive(Debug, Deserialize)]
pub struct CreateTodoRequest {
    pub title: String,
    pub description: Option<String>,
}

/// 更新 Todo 的请求
#[derive(Debug, Deserialize)]
pub struct UpdateTodoRequest {
    pub title: Option<String>,
    pub description: Option<String>,
    pub completed: Option<bool>,
}

/// 查询参数
#[derive(Debug, Deserialize)]
pub struct TodoQuery {
    pub page: Option<u32>,
    pub limit: Option<u32>,
    pub completed: Option<bool>,
    pub search: Option<String>,
}

/// 分页响应
#[derive(Debug, Serialize)]
pub struct PaginatedResponse<T> {
    pub data: Vec<T>,
    pub total: i64,
    pub page: u32,
    pub limit: u32,
    pub total_pages: u32,
}

impl Todo {
    /// 创建新的 Todo
    pub fn new(title: String, description: Option<String>) -> Self {
        let now = chrono::Utc::now().format("%Y-%m-%d %H:%M:%S").to_string();
        Self {
            id: Uuid::new_v4().to_string(),
            title,
            description,
            completed: false,
            created_at: now.clone(),
            updated_at: now,
        }
    }
}

19.6.5 数据库操作(Repository 模式)

// src/handlers/todos.rs(数据库查询部分)
use axum::{
    extract::{Path, Query, State, Json},
    http::StatusCode,
    response::IntoResponse,
};
use sqlx::SqlitePool;
use crate::models::todo::*;
use crate::error::AppError;

/// 获取 Todo 列表
pub async fn list_todos(
    State(pool): State<SqlitePool>,
    Query(query): Query<TodoQuery>,
) -> Result<Json<PaginatedResponse<Todo>>, AppError> {
    let page = query.page.unwrap_or(1);
    let limit = query.limit.unwrap_or(20).min(100);  // 最多 100 条
    let offset = (page - 1) * limit;

    // 查询总数
    let total: (i64,) = if let Some(completed) = query.completed {
        sqlx::query_as("SELECT COUNT(*) FROM todos WHERE completed = ?")
            .bind(completed)
            .fetch_one(&pool)
            .await?
    } else {
        sqlx::query_as("SELECT COUNT(*) FROM todos")
            .fetch_one(&pool)
            .await?
    };

    // 查询数据
    let todos: Vec<Todo> = if let Some(completed) = query.completed {
        sqlx::query_as::<_, Todo>(
            "SELECT * FROM todos WHERE completed = ? ORDER BY created_at DESC LIMIT ? OFFSET ?"
        )
        .bind(completed)
        .bind(limit)
        .bind(offset)
        .fetch_all(&pool)
        .await?
    } else if let Some(search) = &query.search {
        sqlx::query_as::<_, Todo>(
            "SELECT * FROM todos WHERE title LIKE ? ORDER BY created_at DESC LIMIT ? OFFSET ?"
        )
        .bind(format!("%{}%", search))
        .bind(limit)
        .bind(offset)
        .fetch_all(&pool)
        .await?
    } else {
        sqlx::query_as::<_, Todo>(
            "SELECT * FROM todos ORDER BY created_at DESC LIMIT ? OFFSET ?"
        )
        .bind(limit)
        .bind(offset)
        .fetch_all(&pool)
        .await?
    };

    let total_pages = ((total.0 as f64) / (limit as f64)).ceil() as u32;

    Ok(Json(PaginatedResponse {
        data: todos,
        total: total.0,
        page,
        limit,
        total_pages,
    }))
}

/// 获取单个 Todo
pub async fn get_todo(
    State(pool): State<SqlitePool>,
    Path(id): Path<String>,
) -> Result<Json<Todo>, AppError> {
    let todo = sqlx::query_as::<_, Todo>("SELECT * FROM todos WHERE id = ?")
        .bind(&id)
        .fetch_optional(&pool)
        .await?
        .ok_or(AppError::NotFound(format!("Todo {} 不存在", id)))?;

    Ok(Json(todo))
}

/// 创建 Todo
pub async fn create_todo(
    State(pool): State<SqlitePool>,
    Json(payload): Json<CreateTodoRequest>,
) -> Result<(StatusCode, Json<Todo>), AppError> {
    // 验证输入
    if payload.title.trim().is_empty() {
        return Err(AppError::BadRequest("标题不能为空".to_string()));
    }

    if payload.title.len() > 200 {
        return Err(AppError::BadRequest("标题不能超过 200 个字符".to_string()));
    }

    let todo = Todo::new(payload.title, payload.description);

    sqlx::query(
        "INSERT INTO todos (id, title, description, completed, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)"
    )
    .bind(&todo.id)
    .bind(&todo.title)
    .bind(&todo.description)
    .bind(todo.completed)
    .bind(&todo.created_at)
    .bind(&todo.updated_at)
    .execute(&pool)
    .await?;

    tracing::info!("创建 Todo: {} - {}", todo.id, todo.title);

    Ok((StatusCode::CREATED, Json(todo)))
}

/// 更新 Todo
pub async fn update_todo(
    State(pool): State<SqlitePool>,
    Path(id): Path<String>,
    Json(payload): Json<UpdateTodoRequest>,
) -> Result<Json<Todo>, AppError> {
    // 先检查是否存在
    let existing = sqlx::query_as::<_, Todo>("SELECT * FROM todos WHERE id = ?")
        .bind(&id)
        .fetch_optional(&pool)
        .await?
        .ok_or(AppError::NotFound(format!("Todo {} 不存在", id)))?;

    // 更新字段(只更新提供的字段)
    let title = payload.title.unwrap_or(existing.title);
    let description = payload.description.or(existing.description);
    let completed = payload.completed.unwrap_or(existing.completed);
    let now = chrono::Utc::now().format("%Y-%m-%d %H:%M:%S").to_string();

    sqlx::query(
        "UPDATE todos SET title = ?, description = ?, completed = ?, updated_at = ? WHERE id = ?"
    )
    .bind(&title)
    .bind(&description)
    .bind(completed)
    .bind(&now)
    .bind(&id)
    .execute(&pool)
    .await?;

    // 返回更新后的记录
    let updated = sqlx::query_as::<_, Todo>("SELECT * FROM todos WHERE id = ?")
        .bind(&id)
        .fetch_one(&pool)
        .await?;

    tracing::info!("更新 Todo: {}", id);

    Ok(Json(updated))
}

/// 删除 Todo
pub async fn delete_todo(
    State(pool): State<SqlitePool>,
    Path(id): Path<String>,
) -> Result<StatusCode, AppError> {
    let result = sqlx::query("DELETE FROM todos WHERE id = ?")
        .bind(&id)
        .execute(&pool)
        .await?;

    if result.rows_affected() == 0 {
        return Err(AppError::NotFound(format!("Todo {} 不存在", id)));
    }

    tracing::info!("删除 Todo: {}", id);

    Ok(StatusCode::NO_CONTENT)
}

// 对比 Express.js + knex.js:
//
// app.get('/todos', async (req, res) => {
//     const { page = 1, limit = 20 } = req.query;
//     const todos = await knex('todos')
//         .orderBy('created_at', 'desc')
//         .limit(limit)
//         .offset((page - 1) * limit);
//     const [{ count }] = await knex('todos').count('* as count');
//     res.json({ data: todos, total: count, page, limit });
// });
//
// 关键区别:
// 1. Rust 的 SQL 在编译时检查(如果用 query! 宏)
// 2. 参数类型在编译时确认
// 3. 错误必须显式处理
// 4. 不会出现 undefined 或 NaN

19.7 错误处理

19.7.1 统一错误类型

// src/error.rs
use axum::{
    http::StatusCode,
    response::{IntoResponse, Response},
    Json,
};
use serde_json::json;
use thiserror::Error;

/// 应用错误类型
#[derive(Error, Debug)]
pub enum AppError {
    /// 资源未找到
    #[error("未找到:{0}")]
    NotFound(String),

    /// 请求参数错误
    #[error("请求错误:{0}")]
    BadRequest(String),

    /// 未授权
    #[error("未授权:{0}")]
    Unauthorized(String),

    /// 权限不足
    #[error("权限不足:{0}")]
    Forbidden(String),

    /// 数据库错误
    #[error("数据库错误:{0}")]
    Database(#[from] sqlx::Error),

    /// 内部错误
    #[error("内部错误:{0}")]
    Internal(#[from] anyhow::Error),
}

/// 将 AppError 转换为 HTTP 响应
impl IntoResponse for AppError {
    fn into_response(self) -> Response {
        let (status, message) = match &self {
            AppError::NotFound(msg) => (StatusCode::NOT_FOUND, msg.clone()),
            AppError::BadRequest(msg) => (StatusCode::BAD_REQUEST, msg.clone()),
            AppError::Unauthorized(msg) => (StatusCode::UNAUTHORIZED, msg.clone()),
            AppError::Forbidden(msg) => (StatusCode::FORBIDDEN, msg.clone()),
            AppError::Database(e) => {
                tracing::error!("数据库错误:{:?}", e);
                (
                    StatusCode::INTERNAL_SERVER_ERROR,
                    "数据库操作失败".to_string(),
                )
            }
            AppError::Internal(e) => {
                tracing::error!("内部错误:{:?}", e);
                (
                    StatusCode::INTERNAL_SERVER_ERROR,
                    "服务器内部错误".to_string(),
                )
            }
        };

        let body = json!({
            "error": {
                "code": status.as_u16(),
                "message": message,
            }
        });

        (status, Json(body)).into_response()
    }
}

// 现在处理器可以优雅地返回错误:
// async fn get_todo(...) -> Result<Json<Todo>, AppError> {
//     let todo = find_todo(id).await?.ok_or(AppError::NotFound("Todo 不存在"))?;
//     Ok(Json(todo))
// }

// 对比 Express.js:
// // Express 通常需要一个全局错误处理中间件
// app.use((err, req, res, next) => {
//     console.error(err.stack);
//     const status = err.statusCode || 500;
//     res.status(status).json({
//         error: { code: status, message: err.message }
//     });
// });

19.7.2 验证错误

// 自定义验证逻辑
use serde::Deserialize;

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

impl CreateTodoRequest {
    pub fn validate(&self) -> Result<(), AppError> {
        if self.title.trim().is_empty() {
            return Err(AppError::BadRequest("标题不能为空".to_string()));
        }
        if self.title.len() > 200 {
            return Err(AppError::BadRequest("标题不能超过 200 个字符".to_string()));
        }
        if let Some(desc) = &self.description {
            if desc.len() > 2000 {
                return Err(AppError::BadRequest("描述不能超过 2000 个字符".to_string()));
            }
        }
        Ok(())
    }
}

// 在处理器中使用
async fn create_todo(
    State(pool): State<SqlitePool>,
    Json(payload): Json<CreateTodoRequest>,
) -> Result<(StatusCode, Json<Todo>), AppError> {
    payload.validate()?;  // 验证失败自动返回 400
    // ... 创建逻辑
    todo!()
}

use crate::error::AppError;
use crate::models::todo::Todo;
use axum::extract::State;
use axum::http::StatusCode;
use axum::extract::Json;
use sqlx::SqlitePool;

19.8 完整项目组装

19.8.1 src/main.rs —— 入口文件

// src/main.rs
use axum::{
    routing::{get, post, put, delete},
    middleware,
    Router,
};
use tower_http::{
    cors::{CorsLayer, Any},
    trace::TraceLayer,
};
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};

mod db;
mod error;
mod handlers;
mod models;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    // 初始化日志
    tracing_subscriber::registry()
        .with(tracing_subscriber::EnvFilter::new(
            std::env::var("RUST_LOG").unwrap_or_else(|_| "todo_api=debug,tower_http=debug".to_string()),
        ))
        .with(tracing_subscriber::fmt::layer())
        .init();

    // 加载环境变量
    dotenvy::dotenv().ok();

    // 连接数据库
    let database_url = std::env::var("DATABASE_URL")
        .unwrap_or_else(|_| "sqlite:todos.db".to_string());
    let pool = db::create_pool(&database_url).await?;

    // 构建路由
    let app = Router::new()
        // 健康检查
        .route("/health", get(|| async { "OK" }))
        // API 路由
        .nest("/api", api_routes())
        // 注入数据库连接池
        .with_state(pool)
        // 全局中间件
        .layer(TraceLayer::new_for_http())
        .layer(CorsLayer::new()
            .allow_origin(Any)
            .allow_methods(Any)
            .allow_headers(Any));

    // 启动服务器
    let port = std::env::var("PORT").unwrap_or_else(|_| "3000".to_string());
    let addr = format!("0.0.0.0:{}", port);
    let listener = tokio::net::TcpListener::bind(&addr).await?;

    tracing::info!("🚀 服务器启动在 http://{}", addr);
    tracing::info!("📖 API 文档:http://{}/api/todos", addr);

    axum::serve(listener, app).await?;

    Ok(())
}

fn api_routes() -> Router<sqlx::SqlitePool> {
    Router::new()
        .route("/todos", get(handlers::todos::list_todos).post(handlers::todos::create_todo))
        .route("/todos/{id}",
            get(handlers::todos::get_todo)
            .put(handlers::todos::update_todo)
            .delete(handlers::todos::delete_todo))
}

// 目录结构中的 mod 声明
// src/handlers/mod.rs:
// pub mod todos;
//
// src/models/mod.rs:
// pub mod todo;

19.8.2 运行项目

# 开发模式运行(使用 cargo-watch 自动重新编译)
cargo install cargo-watch
cargo watch -x run

# 或者直接运行
cargo run

# 测试 API
# 创建 Todo
curl -X POST http://localhost:3000/api/todos \
  -H "Content-Type: application/json" \
  -d '{"title": "学习 Rust", "description": "完成第19章"}'

# 获取所有 Todo
curl http://localhost:3000/api/todos

# 获取单个 Todo
curl http://localhost:3000/api/todos/<id>

# 更新 Todo
curl -X PUT http://localhost:3000/api/todos/<id> \
  -H "Content-Type: application/json" \
  -d '{"completed": true}'

# 删除 Todo
curl -X DELETE http://localhost:3000/api/todos/<id>

# 分页和搜索
curl "http://localhost:3000/api/todos?page=1&limit=5"
curl "http://localhost:3000/api/todos?completed=false"
curl "http://localhost:3000/api/todos?search=Rust"

19.9 测试

19.9.1 单元测试

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_todo_creation() {
        let todo = Todo::new("测试".to_string(), Some("描述".to_string()));
        assert!(!todo.id.is_empty());
        assert_eq!(todo.title, "测试");
        assert_eq!(todo.description, Some("描述".to_string()));
        assert!(!todo.completed);
    }

    #[test]
    fn test_create_todo_validation() {
        // 空标题
        let req = CreateTodoRequest {
            title: "".to_string(),
            description: None,
        };
        assert!(req.validate().is_err());

        // 正常标题
        let req = CreateTodoRequest {
            title: "学习 Rust".to_string(),
            description: None,
        };
        assert!(req.validate().is_ok());

        // 超长标题
        let req = CreateTodoRequest {
            title: "a".repeat(201),
            description: None,
        };
        assert!(req.validate().is_err());
    }

    use crate::models::todo::*;
    use crate::error::AppError;
}

19.9.2 集成测试

// tests/api_tests.rs
use axum::{
    body::Body,
    http::{Request, StatusCode, Method},
};
use tower::ServiceExt;  // for `oneshot`
use serde_json::{json, Value};

// 辅助函数:创建测试应用
async fn test_app() -> axum::Router {
    let pool = sqlx::SqlitePool::connect("sqlite::memory:")
        .await
        .unwrap();

    // 运行迁移
    sqlx::migrate!("./migrations")
        .run(&pool)
        .await
        .unwrap();

    // 使用和主应用相同的路由配置
    axum::Router::new()
        .route("/api/todos",
            axum::routing::get(todo_api::handlers::todos::list_todos)
            .post(todo_api::handlers::todos::create_todo))
        .route("/api/todos/{id}",
            axum::routing::get(todo_api::handlers::todos::get_todo)
            .put(todo_api::handlers::todos::update_todo)
            .delete(todo_api::handlers::todos::delete_todo))
        .with_state(pool)
}

#[tokio::test]
async fn test_create_and_get_todo() {
    let app = test_app().await;

    // 创建 Todo
    let create_response = app
        .clone()
        .oneshot(
            Request::builder()
                .method(Method::POST)
                .uri("/api/todos")
                .header("content-type", "application/json")
                .body(Body::from(
                    serde_json::to_string(&json!({
                        "title": "测试 Todo",
                        "description": "这是一个测试"
                    })).unwrap()
                ))
                .unwrap()
        )
        .await
        .unwrap();

    assert_eq!(create_response.status(), StatusCode::CREATED);

    let body = axum::body::to_bytes(create_response.into_body(), usize::MAX).await.unwrap();
    let todo: Value = serde_json::from_slice(&body).unwrap();
    assert_eq!(todo["title"], "测试 Todo");
    assert_eq!(todo["completed"], false);

    let todo_id = todo["id"].as_str().unwrap();

    // 获取 Todo
    let get_response = app
        .oneshot(
            Request::builder()
                .uri(format!("/api/todos/{}", todo_id))
                .body(Body::empty())
                .unwrap()
        )
        .await
        .unwrap();

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

#[tokio::test]
async fn test_list_todos_empty() {
    let app = test_app().await;

    let response = app
        .oneshot(
            Request::builder()
                .uri("/api/todos")
                .body(Body::empty())
                .unwrap()
        )
        .await
        .unwrap();

    assert_eq!(response.status(), StatusCode::OK);
    let body = axum::body::to_bytes(response.into_body(), usize::MAX).await.unwrap();
    let result: Value = serde_json::from_slice(&body).unwrap();
    assert_eq!(result["total"], 0);
    assert_eq!(result["data"].as_array().unwrap().len(), 0);
}

#[tokio::test]
async fn test_get_nonexistent_todo() {
    let app = test_app().await;

    let response = app
        .oneshot(
            Request::builder()
                .uri("/api/todos/nonexistent-id")
                .body(Body::empty())
                .unwrap()
        )
        .await
        .unwrap();

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

#[tokio::test]
async fn test_create_todo_validation() {
    let app = test_app().await;

    // 空标题
    let response = app
        .oneshot(
            Request::builder()
                .method(Method::POST)
                .uri("/api/todos")
                .header("content-type", "application/json")
                .body(Body::from(
                    serde_json::to_string(&json!({
                        "title": "",
                    })).unwrap()
                ))
                .unwrap()
        )
        .await
        .unwrap();

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

#[tokio::test]
async fn test_update_todo() {
    let app = test_app().await;

    // 先创建
    let create_resp = app
        .clone()
        .oneshot(
            Request::builder()
                .method(Method::POST)
                .uri("/api/todos")
                .header("content-type", "application/json")
                .body(Body::from(
                    serde_json::to_string(&json!({
                        "title": "原始标题",
                    })).unwrap()
                ))
                .unwrap()
        )
        .await
        .unwrap();

    let body = axum::body::to_bytes(create_resp.into_body(), usize::MAX).await.unwrap();
    let todo: Value = serde_json::from_slice(&body).unwrap();
    let id = todo["id"].as_str().unwrap();

    // 更新
    let update_resp = app
        .clone()
        .oneshot(
            Request::builder()
                .method(Method::PUT)
                .uri(format!("/api/todos/{}", id))
                .header("content-type", "application/json")
                .body(Body::from(
                    serde_json::to_string(&json!({
                        "title": "更新后的标题",
                        "completed": true,
                    })).unwrap()
                ))
                .unwrap()
        )
        .await
        .unwrap();

    assert_eq!(update_resp.status(), StatusCode::OK);
    let body = axum::body::to_bytes(update_resp.into_body(), usize::MAX).await.unwrap();
    let updated: Value = serde_json::from_slice(&body).unwrap();
    assert_eq!(updated["title"], "更新后的标题");
    assert_eq!(updated["completed"], true);
}

#[tokio::test]
async fn test_delete_todo() {
    let app = test_app().await;

    // 先创建
    let create_resp = app
        .clone()
        .oneshot(
            Request::builder()
                .method(Method::POST)
                .uri("/api/todos")
                .header("content-type", "application/json")
                .body(Body::from(
                    serde_json::to_string(&json!({
                        "title": "要删除的",
                    })).unwrap()
                ))
                .unwrap()
        )
        .await
        .unwrap();

    let body = axum::body::to_bytes(create_resp.into_body(), usize::MAX).await.unwrap();
    let todo: Value = serde_json::from_slice(&body).unwrap();
    let id = todo["id"].as_str().unwrap();

    // 删除
    let delete_resp = app
        .clone()
        .oneshot(
            Request::builder()
                .method(Method::DELETE)
                .uri(format!("/api/todos/{}", id))
                .body(Body::empty())
                .unwrap()
        )
        .await
        .unwrap();

    assert_eq!(delete_resp.status(), StatusCode::NO_CONTENT);

    // 确认已删除
    let get_resp = app
        .oneshot(
            Request::builder()
                .uri(format!("/api/todos/{}", id))
                .body(Body::empty())
                .unwrap()
        )
        .await
        .unwrap();

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

// 对比 JavaScript 测试 (supertest + jest):
//
// const request = require('supertest');
// const app = require('./app');
//
// describe('POST /api/todos', () => {
//     it('should create a todo', async () => {
//         const res = await request(app)
//             .post('/api/todos')
//             .send({ title: '测试 Todo' })
//             .expect(201);
//
//         expect(res.body.title).toBe('测试 Todo');
//         expect(res.body.completed).toBe(false);
//     });
// });
//
// Axum 测试的优势:
// 1. 使用内存数据库,不需要启动服务器
// 2. 直接测试路由,不经过网络
// 3. 编译时检查测试代码

19.10 部署

19.10.1 Dockerfile

# 多阶段构建 —— 最终镜像只有几 MB

# 第一阶段:构建
FROM rust:1.75 as builder

WORKDIR /app

# 先只复制依赖文件(利用 Docker 缓存层)
COPY Cargo.toml Cargo.lock ./
RUN mkdir src && echo "fn main() {}" > src/main.rs
RUN cargo build --release
RUN rm src/main.rs

# 复制源代码并重新编译
COPY . .
RUN touch src/main.rs  # 确保重新编译
RUN cargo build --release

# 第二阶段:运行
FROM debian:bookworm-slim

# 安装运行时依赖
RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/*

WORKDIR /app

# 只复制编译好的二进制文件
COPY --from=builder /app/target/release/todo-api .
COPY --from=builder /app/migrations ./migrations

# 设置环境变量
ENV PORT=3000
ENV DATABASE_URL=sqlite:todos.db
ENV RUST_LOG=todo_api=info

EXPOSE 3000

CMD ["./todo-api"]
# 构建镜像
docker build -t todo-api .

# 运行容器
docker run -p 3000:3000 -v ./data:/app/data -e DATABASE_URL=sqlite:/app/data/todos.db todo-api

19.10.2 对比部署大小

┌──────────────────────────────────────────────────────────────┐
│                    部署对比                                    │
├──────────────┬───────────────────┬───────────────────────────┤
│  指标         │  Rust (Axum)      │  Node.js (Express)        │
├──────────────┼───────────────────┼───────────────────────────┤
│  Docker 镜像  │  ~20 MB           │  ~200 MB                  │
│  二进制大小    │  ~5 MB            │  N/A (需要 Node.js)       │
│  内存占用      │  ~5-10 MB         │  ~50-100 MB               │
│  启动时间      │  ~10 ms           │  ~500 ms                  │
│  运行时依赖    │  无               │  Node.js + npm 包         │
│  冷启动        │  几乎没有          │  明显延迟                  │
│  并发性能      │  ~200K req/s      │  ~15K req/s               │
└──────────────┴───────────────────┴───────────────────────────┘

19.10.3 systemd 服务

# /etc/systemd/system/todo-api.service
[Unit]
Description=Todo API Server
After=network.target

[Service]
Type=simple
User=www-data
WorkingDirectory=/opt/todo-api
ExecStart=/opt/todo-api/todo-api
Environment=PORT=3000
Environment=DATABASE_URL=sqlite:/opt/todo-api/data/todos.db
Environment=RUST_LOG=todo_api=info
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target
# 部署步骤
sudo cp todo-api /opt/todo-api/
sudo cp -r migrations /opt/todo-api/
sudo systemctl enable todo-api
sudo systemctl start todo-api
sudo systemctl status todo-api

# 查看日志
sudo journalctl -u todo-api -f

19.10.4 Nginx 反向代理

# /etc/nginx/sites-available/todo-api
server {
    listen 80;
    server_name api.example.com;

    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

19.11 本章总结

┌──────────────────────────────────────────────────────────────────┐
│                  Axum Web API 开发全流程                          │
├──────────────────────────────────────────────────────────────────┤
│                                                                  │
│  1. 框架选择:Axum(Tokio 生态、类型安全、高性能)                  │
│                                                                  │
│  2. 核心概念                                                      │
│     ├── 路由:Router::new().route("/path", get(handler))          │
│     ├── 提取器:Path, Query, Json, State                         │
│     ├── 响应:impl IntoResponse                                  │
│     └── 中间件:Tower Layer                                      │
│                                                                  │
│  3. 数据层                                                       │
│     ├── SQLx:编译时检查的 SQL                                    │
│     ├── 迁移:sqlx migrate                                       │
│     └── 连接池:SqlitePool                                       │
│                                                                  │
│  4. 错误处理                                                      │
│     ├── thiserror:定义错误类型                                    │
│     ├── IntoResponse:错误转 HTTP 响应                            │
│     └── anyhow:内部错误包装                                      │
│                                                                  │
│  5. 测试                                                         │
│     ├── 内存数据库:sqlite::memory:                               │
│     └── tower::ServiceExt::oneshot                               │
│                                                                  │
│  6. 部署                                                         │
│     ├── Docker 多阶段构建(~20MB 镜像)                           │
│     ├── systemd 服务                                             │
│     └── Nginx 反向代理                                            │
│                                                                  │
│  Express.js → Axum 速查表:                                      │
│  ┌─────────────────────┬──────────────────────────────────┐      │
│  │ Express              │ Axum                            │      │
│  ├─────────────────────┼──────────────────────────────────┤      │
│  │ app.get()            │ Router::new().route()           │      │
│  │ req.params           │ Path(param)                     │      │
│  │ req.query            │ Query(query)                    │      │
│  │ req.body             │ Json(body)                      │      │
│  │ res.json()           │ Json(data)                      │      │
│  │ res.status(201)      │ (StatusCode::CREATED, Json(..)) │      │
│  │ app.use(middleware)  │ .layer(middleware)               │      │
│  │ app.locals           │ State(state)                    │      │
│  │ next(err)            │ Result<T, AppError>             │      │
│  │ app.listen(port)     │ axum::serve(listener, app)      │      │
│  └─────────────────────┴──────────────────────────────────┘      │
│                                                                  │
└──────────────────────────────────────────────────────────────────┘

🎉 恭喜! 你已经学会了用 Rust 构建完整的 Web API!从路由定义到数据库操作,从错误处理到部署上线,你掌握了一个 Rust 后端开发者需要的核心技能。

📝 接下来可以尝试:

  • 添加 JWT 认证
  • 集成 Redis 缓存
  • 添加 WebSocket 支持
  • 使用 OpenAPI/Swagger 生成文档
  • 实现速率限制
  • 添加 GraphQL 支持