Web 与数据库 advanced 35 分钟 更新 2026-06-15

Web 服务:axum

用 axum 构建路由和 JSON 接口,并用 oneshot 测试。

Web 服务:axum

你会学到什么

  • Router 把路径映射到异步 handler。
  • 用提取器(extractor)如 Path 取出请求数据,用 Json 返回结构化响应。
  • toweroneshot 在不开端口的情况下测试整个应用。

最小示例

fn app() -> Router {
    Router::new()
        .route("/", get(root))
        .route("/greet/{name}", get(greet))
}

运行代码

cd examples
cargo run -p rt_37_axum   # 监听 http://127.0.0.1:3000
cargo test -p rt_37_axum

依赖:

[dependencies]
axum = "0.8"
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
serde = { version = "1", features = ["derive"] }

[dev-dependencies]
tower = { version = "0.5", features = ["util"] }
http-body-util = "0.1"

代码讲解

handler 是普通的 async 函数,参数就是提取器。Path(name) 从 URL 里取出 {name},返回 Json<T> 会自动序列化并设置 Content-Type:

async fn greet(Path(name): Path<String>) -> Json<Greeting> {
    Json(Greeting { message: format!("Hello, {name}!") })
}

把路由抽成 app() -> Router,测试时就能用 oneshot 直接把请求喂给应用、检查响应,完全不需要真实网络:

let response = app()
    .oneshot(Request::builder().uri("/greet/Rust").body(Body::empty()).unwrap())
    .await
    .unwrap();
assert_eq!(response.status(), StatusCode::OK);

常见错误

axum 0.8 的路径参数用花括号 {name},旧版本用 :name。版本不匹配会导致路由注册 panic。

练习

  • 加一个 POST /tasks 接口,用 Json 提取请求体。
  • greet 加一个查询参数 ?lang=en,用 Query 提取器读取。

小结

axum 把路由、提取器和异步 handler 组合成类型安全的 Web 框架,oneshot 让端到端测试既快又简单。

下一步

真实服务需要持久化数据。下一章用 SQLite 做数据库操作。

完整示例代码

下面是 examples/37_axum/ 的完整源码。无需 clone 仓库,直接在页面上阅读、复制、对照运行。

examples/37_axum/src/main.rs
//! 用 axum 构建一个最小 HTTP 服务。

use axum::{Json, Router, extract::Path, routing::get};
use serde::Serialize;

#[derive(Serialize)]
struct Greeting {
    message: String,
}

async fn root() -> &'static str {
    "Rust Tutorial API"
}

/// 路径参数 `{name}` 被提取进 handler。
async fn greet(Path(name): Path<String>) -> Json<Greeting> {
    Json(Greeting {
        message: format!("Hello, {name}!"),
    })
}

/// 把路由组装成一个 Router,方便测试时复用。
fn app() -> Router {
    Router::new()
        .route("/", get(root))
        .route("/greet/{name}", get(greet))
}

#[tokio::main]
async fn main() {
    let listener = tokio::net::TcpListener::bind("127.0.0.1:3000")
        .await
        .expect("无法绑定端口");
    println!("listening on http://127.0.0.1:3000");
    axum::serve(listener, app()).await.expect("服务退出");
}

#[cfg(test)]
mod tests {
    use super::*;
    use axum::body::Body;
    use axum::http::{Request, StatusCode};
    use http_body_util::BodyExt;
    use tower::ServiceExt; // 提供 oneshot

    #[tokio::test]
    async fn root_returns_text() {
        let response = app()
            .oneshot(Request::builder().uri("/").body(Body::empty()).unwrap())
            .await
            .unwrap();
        assert_eq!(response.status(), StatusCode::OK);
        let body = response.into_body().collect().await.unwrap().to_bytes();
        assert_eq!(&body[..], b"Rust Tutorial API");
    }

    #[tokio::test]
    async fn greet_returns_json() {
        let response = app()
            .oneshot(
                Request::builder()
                    .uri("/greet/Rust")
                    .body(Body::empty())
                    .unwrap(),
            )
            .await
            .unwrap();
        assert_eq!(response.status(), StatusCode::OK);
        let body = response.into_body().collect().await.unwrap().to_bytes();
        assert_eq!(&body[..], br#"{"message":"Hello, Rust!"}"#);
    }

    #[tokio::test]
    async fn unknown_route_404() {
        let response = app()
            .oneshot(
                Request::builder()
                    .uri("/missing")
                    .body(Body::empty())
                    .unwrap(),
            )
            .await
            .unwrap();
        assert_eq!(response.status(), StatusCode::NOT_FOUND);
    }
}
examples/37_axum/Cargo.toml
[package]
name = "rt_37_axum"
version.workspace = true
edition.workspace = true
publish.workspace = true

[dependencies]
axum = "0.8.9"
serde = { version = "1.0.228", features = ["derive"] }
tokio = { version = "1.52.3", features = ["rt-multi-thread", "macros"] }

[dev-dependencies]
http-body-util = "0.1.3"
tower = { version = "0.5.3", features = ["util"] }