平时闲暇时,不少程序员喜欢写点小项目:网页、桌面应用、App、小程序……写完以后总想部署到服务器上,给别人看看效果,或者提供对外服务。
对外服务通常的做法,是把应用后端的核心部分部署到一台服务器上。可国内服务器资源并不便宜。我个人以前习惯用 java + springboot 快速写业务,但 Java 本身比较占内存,SpringBoot 作为其中的 starter 框架更是吃内存大户,哪怕只是一个很简单的 demo。

当然,对企业来说内存不算什么大问题。但对我个人来说,问题就大了:我服务器都是每年 618、双十一 各大云服务商搞活动时捡的“破烂”,配置基本就是 1核1G 这种。一台服务器上,跑不了几个小 demo。
所以我开始尝试换语言开发,先后看了 Go 和 Rust,最后选择了 Rust。Rust 这几年热度不低,它是一门速度极快、高性能、注重内存安全的静态编译型语言。而 Axum 是 Rust 生态中一个专注于性能和简单性的 Web框架,用起来很顺手,也符合我的需求。在官方文档里踩了不少坑的同时,我顺手整理了一系列教程;目前网上关于 Axum 的资料也不多,就免费分享出来。
下面,我们就从本文开始 Rust + Axum 的 Web 开发之旅。建议最好懂一点 Rust 基础,不用难,多入几次门自然就熟了。
什么是 Axum?
Axum 利用了 hyper 库的功能来增强 Web 应用程序的速度和并发性。同时,Axum 通过与 Tokio 库集成,将 Rust 的 async/await 能力推到了前台,让开发者可以开发高性能的异步 API 和 Web 应用程序。

Axum 是一个注重人体工程学和模块化的 Web 应用程序框架,它有几个主要特点:
- 使用无宏的 API 实现路由(router)功能
- 使用提取器(extractor)对请求进行声明式解析
- 简单、可预测的错误处理模式
- 用最少的模板生成响应
- 充分利用
tower 和 tower-http 的中间件、服务和工具生态系统
Axum 的基本功能基于 Tokio runtime,这给了 Rust 管理非阻塞、事件驱动活动的能力。这种能力对于平稳处理多个并发进程至关重要。
Axum 与现有框架不同的地方在于,它没有自己的中间件系统,而是直接使用 tower::Service。这意味着 Axum 可以无成本地获得超时、跟踪、压缩、授权等能力,还能与使用 hyper 或 tonic 编写的应用共享中间件。
此外,Axum 基于 Rust 强大的类型系统和所有权规则构建,这些规则在编译时就能防止数据竞争、内存泄漏等常见 Web 开发陷阱。模块化的设计也允许开发者只引入必要组件,创建出轻量、专注的应用。
创建一个项目
首先创建一个 Rust 项目。这里默认你已经装好 Rust 开发环境,具体安装步骤可以直接去官网查看。
cargo new rust_axum_web_guide
编写第一个 hello world
先在 Cargo.toml 中添加依赖:
[package]
name = "rust_axum_web_guide"
version = "0.1.0"
edition = "2021"
[dependencies]
axum = "0.7.9"
tokio = { version = "1.0", features = ["full"] }
axum 选择当前最新的 0.7.9 版本;再引入 Tokio,以便充分利用它的生态。
接下来用官网的 hello world 示例改写 main.rs:
use axum::{
routing::get,
Router,
};
#[tokio::main]
async fn main() {
// build our application with a single route
let app = Router::new().route("/", get(|| async { "Hello, World!" }));
// run our app with hyper, listening globally on port 3000
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
其中,tokio::main 属性宏表明 Axum 其实就是一个 Tokio 应用。Tokio 是 Rust 非常优秀的 异步运行时框架,提供了编写异步网络服务所需的几乎所有功能。
Router::new().route 创建路由,tokio::net::TcpListener::bind 监听服务器地址,并把生成的 listener 传入 axum::serve(),启动服务。
启动项目:
cargo run
编译并运行当前项目,这个命令还会自动下载依赖。如果一切顺利,终端会输出 Running on http://localhost:3000。在浏览器中访问 http://localhost:3000,或者用 curl 访问,页面会显示 Hello, World!。
路由与处理器
Router 用来设置哪些路径指向哪些服务,它是一个用于组合处理程序和服务的结构体。路由机制负责把传入的 HTTP 请求定向到指定的 handler,而 handler 就是应用逻辑真正存在的地方。
先看一个例子:
use axum::{
routing::get,
Router,
};
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/", get(|| async { "Hello, World!" }))
.route("/foo", get(get_foo).post(post_foo))
.route("/foo/bar", get(foo_bar));
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
async fn get_foo() -> String {
return String::from("get请求foo方法")
}
async fn post_foo() -> String {
return String::from("post请求foo方法")
}
async fn foo_bar() -> String {
return String::from("get请求->foo_bar")
}
路由和 handler 绑定后,handler 类似 Java Spring 中的 controller。处理器 handler 是一个异步函数或异步代码块,它接受零个或多个 extractors 作为参数,并返回一些可以转换为 IntoResponse 的内容。
也可以将路由和多个 handler 绑定,比如 .route("/foo", get(get_foo).post(post_foo)) 同时绑定了 GET 和 POST 方法;当然还可以指定 PUT、DELETE 等。
处理器更详细的信息,可见 axum::handler 文档。
用 postman 访问 /foo,分别发送 GET 或 POST 请求,可以得到不同的结果。
如果有多个完全相同的路由,程序会 panic。
路由更详细信息,可见 Router in axum 文档。
use axum::{routing::get, Router};
let app = Router::new()
.route("/", get(|| async {}))
.route("/", get(|| async {}));
提取器
提取器 Extractors 用于分离传入请求,以获得处理程序所需的部分,比如解析异步函数的参数。
捕获动态 URL 值以及查询参数
先看如何捕获动态 URL 值和查询参数,主要使用 path 与 query 提取器。
use axum::{
routing::get,
Router,
extract::{Path, Query}
};
use std::collections::HashMap;
use serde::Deserialize;
#[derive(Deserialize)]
struct Page {
number: u32,
}
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/", get(|| async { "Hello, World!" }))
.route("/item/:id", get(print_item))
.route("/item2/:name/:age", get(print_item_handler2))
.route("/item3/:fullpath", get(print_item_handler3))
.route("/query/:id", get(print_query));
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
async fn print_item(Path(_id): Path<u32>) -> String {
format!("print_item: {}", _id)
}
async fn print_item_handler2(Path((name, age)): Path<(String, i64)>) -> String {
format!("name: {name}, age: {age}")
}
async fn print_item_handler3(Path(fullpath): Path<HashMap<String, String>>) -> String {
format!("path: /{fullpath:?}")
}
async fn print_query(Path(id): Path<u32>, Query(page): Query<Page>) -> String {
format!("Query {} on page {}", id, page.number)
}
还需要添加依赖来支持序列化:
serde = { version = "1.0.216", features = ["derive"] }
测试结果:
/item/321 # 结果 print_item: 321
/item2/zj/18 # 结果 name: zj, age: 18
/item3/{name: zzjj} # 结果 path: /{"fullpath": "{name: zzjj}"}
/query/10?number=31 # 结果 Query 10 on page 31
我们还能将多个参数写入到 hashmap 中:
use axum::{
routing::get,
Router,
extract::Query
};
use serde::Deserialize;
use std::collections::HashMap;
#[derive(Deserialize, Debug)]
struct Info {
name: String,
age: u8,
}
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/", get(|| async { "Hello, World!" }))
.route("/query_1", get(print_map_handler))
.route("/query_2", get(print_map2_handler));
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
async fn print_map_handler(query: Query<Info>) -> String {
let info: Info = query.0;
format!("query1: {info:?}")
}
async fn print_map2_handler(query: Query<HashMap<String, String>>) -> String {
format!("query2: {query:?}")
}
测试结果:
/query_1?name=zj&age=18 # result query1: Info { name: "zj", age: 18 }
/query_2?name=zj&age=18 # result query2: Query({"age": "18", "name": "zj"})
path 和 query 提取器一般适用于 GET 请求,那 POST 请求的参数呢?
捕获请求体的参数
常见的请求体参数一般有两种格式:json 和 form。下面看一个例子:
use axum::{
routing::{get, post},
Router,
extract::{Json, Form}
};
use serde::Deserialize;
#[derive(Deserialize, Debug)]
struct CreateUser {
username: String,
age: u8
}
#[derive(Deserialize, Debug)]
struct LoginFormData {
field_name: String,
field_pwd: String,
}
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/", get(|| async { "Hello, World!" }))
.route("/addUser", post(create_user_handler))
.route("/submit", post(submit_form_handler));
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
async fn create_user_handler(Json(payload): Json<CreateUser>) -> String {
format!("Added user: {payload:?}")
}
async fn submit_form_handler(Form(data): Form<LoginFormData>) -> String {
format!("submit form: {data:?}")
}
测试结果:
post /addUser # result Added user: CreateUser { username: "zsan", age: 18 }
post /submit # result submit form: LoginFormData { field_name: "lis", field_pwd: "123" }
捕获请求头参数
一般通过 HeaderMap 来获取请求头参数,它会提取所有标头;如果想获取指定标头,需要再做一层处理。
use axum::{
routing::get,
Router,
http::header::HeaderMap
};
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/", get(|| async { "Hello, World!" }))
.route("/header", get(get_header_handler));
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
async fn get_header_handler(headers: HeaderMap) -> String {
// format!("{:?}", headers) // 获取所有请求头
headers
.get(axum::http::header::USER_AGENT)
.and_then(|v| v.to_str().ok())
.map(|v| v.to_string())
.unwrap()
}
测试结果:
(get) /header # result PostmanRuntime/7.43.0
除此之外,axum 还提供了许多有用的提取器,比如 Bytes、String、Body、BodyStream 用于获取请求正文。你也可以通过实现 FromRequest 来定义自己的提取器。
响应处理
任何实现 IntoResponse 的内容,都可以作为处理程序的返回值,它会被自动转换为响应。看一个例子:
use axum::{
body::Body,
routing::get,
response::Json,
Router,
};
use serde_json::{Value, json};
// `&'static str` becomes a `200 OK` with `content-type: text/plain; charset=utf-8`
async fn plain_text() -> &'static str {
"foo"
}
// `Json` gives a content-type of `application/json` and works with any type
// that implements `serde::Serialize`
async fn json() -> Json<Value> {
Json(json!({ "data": 42 }))
}
let app = Router::new()
.route("/plain_text", get(plain_text))
.route("/json", get(json));
更多详细例子,可以看官网文档。
错误处理
Axum 的目标是拥有一个简单且可预测的错误处理模型,这意味着将错误转换为响应很简单,并且可以保证所有错误都得到处理。
Axum 基于 tower::Service,它通过关联的错误类型来处理错误。如果你的 Service 产生错误,并且该错误一路传到 hyper,连接就会终止而不发送响应。这通常不可取。为此 Axum 要求所有服务都以 Infallible 作为错误类型——Infallible 表示错误永远不会发生。
这段引用自官方文档,但太绕了。说人话就是:发生错误时,不能让 HTTP 连接中断,否则响应无法正常返回;而是要保证响应能正常返回,并携带错误类型和错误信息。Axum 能很轻易地做到这点。
所以一般 Axum 处理程序绑定的方法会这样定义:
use axum::http::StatusCode;
async fn handler() -> Result<String, StatusCode> {
// ...
}
虽然看起来可能因为 StatusCode 失败,但实际上这并不是一个真正的“错误”。如果此处理程序返回 Err(some_status_code),它仍会被转换为响应并返回给客户端。这是通过 StatusCode 实现的 IntoResponse 完成的。
无论你是返回 Err(StatusCode::NOT_FOUND) 还是 Err(StatusCode::INTERNAL_SERVER_ERROR),在 Axum 中这些都不被视为错误。
不过,更推荐的做法不是直接使用 StatusCode,而是使用中间的错误类型,最终再转换为响应。这样就能在处理程序中使用 ? 运算符。
看一个例子:
use axum::{
routing::get,
Router,
body::Body,
http::{Response, StatusCode},
error_handling::HandleError,
};
use tower;
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/", get(|| async { "Hello, World!" }))
.merge(router_fallible_service()) // 使用 Service 的错误处理
.route("/foo", get(get_foo));
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
fn router_fallible_service() -> Router {
// this service 可能出现任何错误
let some_fallible_service = tower::service_fn(|_req| async {
thing_that_might_fail().await?;
Ok::<_, anyhow::Error>(Response::new(Body::empty()))
});
Router::new().route_service(
"/test_error",
// Service 适配器通过将错误转换为响应来处理错误。
HandleError::new(some_fallible_service, handle_anyhow_error),
)
}
async fn thing_that_might_fail() -> Result<(), anyhow::Error> {
// 模拟报错
anyhow::bail!("thing_that_might_fail")
}
// 将错误转化为 IntoResponse
async fn handle_anyhow_error(err: anyhow::Error) -> (StatusCode, String) {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Something went wrong: {err}"),
)
}
async fn get_foo() -> Result<String, StatusCode> {
Ok(String::from("get请求foo方法"))
}
大家可以自行运行一下,看看返回的响应。要正常运行上述示例,还需要添加依赖:
anyhow = "1.0.95"
tower = { version = "0.5.2", features = ["full"] }
更多其他用法,请移步 axum::error_handling 文档。
中间件
Axum 的独特之处在于它没有自己的定制中间件系统,而是与 Tower 集成。这意味着 tower 和 tower-http 的中间件都可以与 Axum 一起使用。axum 也可以将请求路由到任何 tower 服务,可以是你用 service_fn 编写的服务,也可以是其他 crate 的东西;从而充分复用和利用不同应用的生态,潜力巨大。
中间件允许你在请求到达处理程序之前或之后添加自定义逻辑,这为身份验证、日志记录、性能监控等通用能力提供了强大的实现方式。
这里举一个简单的例子:
use axum::{
body::Body,
http::Request,
middleware::{self, Next},
response::Response,
routing::get,
Router
};
async fn logging_middleware(req: Request<Body>, next: Next) -> Response {
println!("Received a request to {}", req.uri());
next.run(req).await
}
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/", get(|| async { "Hello, world!" }))
.layer(middleware::from_fn(logging_middleware));
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
运行后,用户访问任意请求都会被终端记录下来:
Received a request to /foo
Received a request to /
Received a request to /test
更多例子,请移步 axum::middleware 文档。
本文就先到这里。一个 Web 框架基本解决的就是文章中提到的这些方面:路由、提取器、响应、错误处理和中间件。后续我们会继续探索 Axum 与数据库的集成,以及其他有意思的特性。下期再见。
如果你对 Rust 后端开发感兴趣,也可以到 云栈社区 逛逛,那里有更多 Rust 全栈与后端方向的学习资源。
参考资料: