Web 服务:axum
你会学到什么
- 用
Router把路径映射到异步 handler。 - 用提取器(extractor)如
Path取出请求数据,用Json返回结构化响应。 - 用
tower的oneshot在不开端口的情况下测试整个应用。
最小示例
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"] }