跳到主要内容
Rust Web 开发实战:Axum 框架从路由到数据库 | 极客日志
Rust
Rust Web 开发实战:Axum 框架从路由到数据库 从零起步构建Axum项目,涵盖路由嵌套、路径参数、Json/Query/Path等提取器、响应定制、全局状态管理(State)以及基于Tower的日志与鉴权中间件。生产级示例结合SQLx实现用户CRUD REST API,并对比Actix-web说明Axum依赖类型系统而非宏的设计优点,适合已有Rust异步基础的开发者快速上手。
要用 Rust 写 Web 服务,Axum 是当下很自然的选择——它直接跑在 Tokio 异步运行时上,中间件层复用 Tower,整体风格偏向类型安全,几乎不需要宏。这篇文章不列八股,直接拆核心用法:从搭环境、玩路由,到连数据库跑一个完整的 REST API。
环境准备
先确保 rustc ≥ 1.64,这是 Axum 的最低要求。需要熟悉 Tokio 异步那一套,async/await 得会写。Axum 底层对接 Tower,所以中间件也是 Tower 那套生态。
创建项目
cargo new axum-demo && cd axum-demo
Cargo.toml 里加这些依赖:
[package]
name = "axum-demo"
version = "0.1.0"
edition = "2021"
[dependencies]
axum = "0.7"
tokio = { version = "1.0" , features = ["full" ] }
http = "1.0"
tracing = { version = "0.1" , features = ["log" ] }
tracing-subscriber = { version = "0.3" , features = ["env-filter" ] }
serde = { version = "1.0" , features = ["derive" ] }
serde_json = "1.0"
第一个接口简单一点:监听 0.0.0.0:3000,访问 / 回一句 Hello。
use axum::{routing::get, Router, Server};
use std::net::SocketAddr;
async fn root_handler () -> &'static str {
"Hello, Axum!"
}
#[tokio::main]
async () {
tracing_subscriber::fmt:: ();
= Router:: (). ( , (root_handler));
= SocketAddr:: (([ , , , ], ));
( , addr);
Server:: (&addr). (app. ()). . ();
}
fn
main
init
let
app
new
route
"/"
get
let
addr
from
0
0
0
0
3000
println!
"服务器运行在 http://{}"
bind
serve
into_make_service
await
unwrap
cargo run 起来,浏览器打开 http://localhost:3000 看到 "Hello, Axum!",环境就没问题了。
路由 Router 负责把请求分发到对应的处理函数,支持嵌套、路径参数,方法匹配用 get、post、put、delete 这些函数。
基本用法 use axum::{routing::{get, post}, Router};
async fn get_handler () -> &'static str {
"这是 GET 请求"
}
async fn post_handler () -> &'static str {
"这是 POST 请求"
}
#[tokio::main]
async fn main () {
let app = Router::new ()
.route ("/get" , get (get_handler))
.route ("/post" , post (post_handler));
}
路径参数 用 /:param 占位,处理函数通过提取器拿值,支持 String、u32 这些:
use axum::{extract::Path, routing::get, Router};
use std::collections::HashMap;
async fn user_handler (Path (user_id): Path<u32 >) -> String {
format! ("用户 ID:{}" , user_id)
}
async fn article_handler (Path ((user_id, article_id)): Path<(u32 , String )>) -> String {
format! ("用户 {} 的文章:{}" , user_id, article_id)
}
async fn map_handler (Path (params): Path<HashMap<String , String >>) -> String {
format! ("路径参数:{:?}" , params)
}
#[tokio::main]
async fn main () {
let app = Router::new ()
.route ("/user/:user_id" , get (user_handler))
.route ("/article/:user_id/:article_id" , get (article_handler))
.route ("/map/:k1/:k2" , get (map_handler));
}
http://localhost:3000/user/100 → 用户 ID:100
http://localhost:3000/article/100/rust-axum → 用户 100 的文章:rust-axum
路由嵌套 nest 适合按模块分路由,比如用户模块和文章模块:
use axum::{routing::get, Router};
fn user_routes () -> Router {
Router::new ()
.route ("/" , get (|| async { "用户列表" }))
.route ("/:id" , get (|Path (id): Path<u32 >| async move {
format! ("用户详情:{}" , id)
}))
}
fn article_routes () -> Router {
Router::new ()
.route ("/" , get (|| async { "文章列表" }))
.route ("/:id" , get (|Path (id): Path<String >| async move {
format! ("文章详情:{}" , id)
}))
}
#[tokio::main]
async fn main () {
let app = Router::new ()
.nest ("/user" , user_routes ())
.nest ("/article" , article_routes ());
}
/user 和 /article 各自独立,结构清晰。
提取器 提取器是 Axum 的灵魂:处理函数通过参数类型声明要从请求里拿什么,框架会自动解析。不用手动解请求体,也不用宏。
内置提取器概览 提取器 作用 示例 Path<T>路径参数 Path(user_id): Path<u32>Query<T>URL 查询参数 Query(params): Query<HashMap<String, String>>Json<T>JSON 请求体 Json(user): Json<User>Form<T>表单请求体(application/x-www-form-urlencoded) Form(form): Form<LoginForm>HeaderMap请求头 headers: HeaderMapState<T>应用全局状态 state: State<AppState>
解析 JSON 请求体 use axum::{extract::Json, routing::post, Router};
use serde::Deserialize;
#[derive(Deserialize)]
struct CreateUserRequest {
name: String ,
age: u32 ,
email: String ,
}
async fn create_user (Json (req): Json<CreateUserRequest>) -> String {
format! ("创建用户成功:姓名={}, 年龄={}, 邮箱={}" , req.name, req.age, req.email)
}
#[tokio::main]
async fn main () {
let app = Router::new ().route ("/user" , post (create_user));
}
curl -X POST -H "Content-Type: application/json" -d '{"name":"张三","age":25,"email":"[email protected] "}' http://localhost:3000/user
解析查询参数 use axum::{extract::Query, routing::get, Router};
use serde::Deserialize;
#[derive(Deserialize)]
struct Pagination {
page: u32 ,
size: u32 ,
}
async fn list_users (Query (pagination): Query<Pagination>) -> String {
format! ("查询用户列表:第 {} 页,每页 {} 条" , pagination.page, pagination.size)
}
#[tokio::main]
async fn main () {
let app = Router::new ().route ("/users" , get (list_users));
}
访问 http://localhost:3000/users?page=1&size=10 会输出 查询用户列表:第 1 页,每页 10 条。
组合提取器 use axum::{extract::{Path, Query}, routing::get, Router};
use serde::Deserialize;
#[derive(Deserialize)]
struct Filter {
keyword: String ,
}
async fn search_articles (
Path (user_id): Path<u32 >,
Query (filter): Query<Filter>,
) -> String {
format! ("用户 {} 搜索文章:关键词={}" , user_id, filter.keyword)
}
#[tokio::main]
async fn main () {
let app = Router::new ().route ("/user/:user_id/articles" , get (search_articles));
}
/user/100/articles?keyword=rust 会输出 用户 100 搜索文章:关键词=rust。
响应 Axum 允许处理函数返回多种类型,自动转成 HTTP 响应。常见的:
字符串 → text/plain
Json<T> → application/json
StatusCode → 只回状态码
(StatusCode, Json<T>) → 状态码 + JSON
自定义实现 IntoResponse
use axum::{extract::Path, http::StatusCode, response::Json, routing::get, Router};
use serde::Serialize;
#[derive(Serialize)]
struct User {
id: u32 ,
name: String ,
}
async fn success_response () -> Json<User> {
Json (User {
id: 100 ,
name: "张三" .to_string (),
})
}
async fn error_response () -> StatusCode {
StatusCode::NOT_FOUND
}
async fn custom_response (Path (id): Path<u32 >) -> (StatusCode, String ) {
if id == 100 {
(StatusCode::OK, "请求成功" .to_string ())
} else {
(StatusCode::BAD_REQUEST, "无效 ID" .to_string ())
}
}
#[tokio::main]
async fn main () {
let app = Router::new ()
.route ("/success" , get (success_response))
.route ("/error" , get (error_response))
.route ("/custom/:id" , get (custom_response));
}
如果想自定义响应头,用 ResponseBuilder:
use axum::{http::header, response::IntoResponse, routing::get, Router};
async fn custom_header () -> impl IntoResponse {
([
(header::CONTENT_TYPE, "application/xml" ),
(header::X_POWERED_BY, "Axum" ),
], "<user><id>100</id><name>张三</name></user>" )
}
#[tokio::main]
async fn main () {
let app = Router::new ().route ("/xml" , get (custom_header));
}
全局状态:State 提取器 共享资源(数据库连接池、配置)靠 State 实现。定义一个结构体,用 Arc 包裹,然后 with_state 注入路由。
use axum::{extract::State, routing::get, Router, Server};
use std::net::SocketAddr;
use std::sync::Arc;
#[derive(Clone)]
struct AppState {
app_name: String ,
app_version: String ,
}
async fn get_app_info (State (state): State<Arc<AppState>>) -> String {
format! ("应用名称:{},版本:{}" , state.app_name, state.app_version)
}
#[tokio::main]
async fn main () {
let app_state = Arc::new (AppState {
app_name: "Axum Demo" .to_string (),
app_version: "1.0.0" .to_string (),
});
let app = Router::new ()
.route ("/info" , get (get_app_info))
.with_state (app_state);
let addr = SocketAddr::from (([0 , 0 , 0 , 0 ], 3000 ));
Server::bind (&addr).serve (app.into_make_service ()).await .unwrap ();
}
注意几点:状态结构体必须实现 Clone;重量级资源要用 Arc 避免性能问题;状态默认不可变,要改的话得配合 Mutex 或 RwLock(异步推荐 tokio::sync::Mutex)。
use axum::{extract::State, routing::get, Router, Server};
use std::net::SocketAddr;
use std::sync::Arc;
use tokio::sync::Mutex;
struct AppState {
counter: Mutex<u32 >,
}
async fn increment_counter (State (state): State<Arc<AppState>>) -> String {
let mut counter = state.counter.lock ().await ;
*counter += 1 ;
format! ("当前计数器值:{}" , counter)
}
async fn get_counter (State (state): State<Arc<AppState>>) -> String {
let counter = state.counter.lock ().await ;
format! ("当前计数器值:{}" , counter)
}
#[tokio::main]
async fn main () {
let app_state = Arc::new (AppState {
counter: Mutex::new (0 ),
});
let app = Router::new ()
.route ("/increment" , get (increment_counter))
.route ("/counter" , get (get_counter))
.with_state (app_state);
let addr = SocketAddr::from (([0 , 0 , 0 , 0 ], 3000 ));
Server::bind (&addr).serve (app.into_make_service ()).await .unwrap ();
}
中间件 中间件在请求到达处理函数前或响应返回前做一些事,比如日志、鉴权、压缩。Axum 完全兼容 Tower 生态,可以直接用 Tower 现成的中间件,也能自己写。
日志和压缩 tower-http = { version = "0.5" , features = ["logging" , "compression" ] }
use axum::{routing::get, Router, Server};
use std::net::SocketAddr;
use tower_http::{compression::CompressionLayer, logging::LoggingLayer};
async fn root () -> &'static str {
"Hello, Axum with Middleware!"
}
#[tokio::main]
async fn main () {
tracing_subscriber::fmt::init ();
let app = Router::new ()
.route ("/" , get (root))
.layer (LoggingLayer::new ())
.layer (CompressionLayer::new ());
let addr = SocketAddr::from (([0 , 0 , 0 , 0 ], 3000 ));
Server::bind (&addr).serve (app.into_make_service ()).await .unwrap ();
}
自定义中间件:鉴权 写个函数检查 Authorization 头,不通过就回 401:
use axum::{body::Body, extract::Request, http::{header::AUTHORIZATION, StatusCode}, middleware::Next, response::Response, routing::get, Router};
async fn auth_middleware (mut req: Request<Body>, next: Next) -> Result <Response, StatusCode> {
let auth_header = req.headers ().get (AUTHORIZATION);
if let Some (token) = auth_header {
if token == "Bearer axum-token" {
return Ok (next.run (req).await );
}
}
Err (StatusCode::UNAUTHORIZED)
}
async fn protected_route () -> &'static str {
"这是受保护的路由,鉴权成功!"
}
#[tokio::main]
async fn main () {
let app = Router::new ()
.route ("/protected" , get (protected_route))
.route_layer (axum::middleware::from_fn (auth_middleware));
}
携带 -H "Authorization: Bearer axum-token" 就能访问,否则 401。
中间件可以全局加(.layer),也可以只作用于特定路由(.route_layer),按需使用。
生产级实战:Axum + SQLx 构建 RESTful API 现在做一个用户管理的 CRUD,整合 Postgres 和 SQLx。
依赖准备 sqlx = { version = "0.7" , features = ["postgres" , "runtime-tokio-native-tls" , "macros" , "json" ] }
dotenv = "0.15"
DATABASE_URL =postgres://username:password@localhost:5432 /axum_demo
数据库初始化 mkdir -p migrations
echo "CREATE TABLE users ( id SERIAL PRIMARY KEY, name VARCHAR(50) NOT NULL, age INT NOT NULL, email VARCHAR(100) UNIQUE NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP );" > migrations/0001_create_users_table.up.sql
echo "DROP TABLE users;" > migrations/0001_create_users_table.down.sql
sqlx migrate run --database-url postgres://username:password@localhost:5432/axum_demo
CRUD 实现 use axum::{
extract::{Json, Path, Query, State},
http::StatusCode,
middleware::from_fn,
response::IntoResponse,
routing::{delete, get, post, put},
Router, Server,
};
use dotenv::dotenv;
use serde::{Deserialize, Serialize};
use sqlx::{postgres::PgPoolOptions, PgPool};
use std::collections::HashMap;
use std::env;
use std::net::SocketAddr;
use std::sync::Arc;
struct AppState {
db_pool: PgPool,
}
#[derive(Debug, Serialize, Deserialize, sqlx::FromRow)]
struct User {
id: i32 ,
name: String ,
age: i32 ,
email: String ,
}
#[derive(Debug, Deserialize)]
struct CreateUserRequest {
name: String ,
age: i32 ,
email: String ,
}
#[derive(Debug, Deserialize)]
struct UpdateUserRequest {
name: Option <String >,
age: Option <i32 >,
email: Option <String >,
}
#[derive(Debug, Serialize)]
struct ErrorResponse {
message: String ,
}
enum AppError {
DatabaseError (sqlx::Error),
NotFound,
BadRequest (String ),
}
impl IntoResponse for AppError {
fn into_response (self ) -> axum::response::Response {
let (status, message) = match self {
AppError::DatabaseError (e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string ()),
AppError::NotFound => (StatusCode::NOT_FOUND, "资源不存在" .to_string ()),
AppError::BadRequest (msg) => (StatusCode::BAD_REQUEST, msg),
};
(status, Json (ErrorResponse { message })).into_response ()
}
}
impl From <sqlx::Error> for AppError {
fn from (err: sqlx::Error) -> Self {
match err {
sqlx::Error::RowNotFound => AppError::NotFound,
_ => AppError::DatabaseError (err),
}
}
}
async fn create_user (
State (state): State<Arc<AppState>>,
Json (req): Json<CreateUserRequest>,
) -> Result <impl IntoResponse , AppError> {
let user = sqlx::query_as!(
User,
"INSERT INTO users (name, age, email) VALUES ($1, $2, $3) RETURNING *" ,
req.name,
req.age,
req.email
)
.fetch_one (&state.db_pool)
.await ?;
Ok ((StatusCode::CREATED, Json (user)))
}
async fn list_users (
State (state): State<Arc<AppState>>,
Query (params): Query<HashMap<String , String >>,
) -> Result <impl IntoResponse , AppError> {
let page = params.get ("page" ).map (|p| p.parse::<i32 >().unwrap_or (1 )).unwrap_or (1 );
let size = params.get ("size" ).map (|s| s.parse::<i32 >().unwrap_or (10 )).unwrap_or (10 );
let offset = (page - 1 ) * size;
let users = sqlx::query_as!(
User,
"SELECT * FROM users LIMIT $1 OFFSET $2" ,
size,
offset
)
.fetch_all (&state.db_pool)
.await ?;
Ok (Json (users))
}
async fn get_user (
State (state): State<Arc<AppState>>,
Path (user_id): Path<i32 >,
) -> Result <impl IntoResponse , AppError> {
let user = sqlx::query_as!(
User,
"SELECT * FROM users WHERE id = $1" ,
user_id
)
.fetch_one (&state.db_pool)
.await ?;
Ok (Json (user))
}
async fn update_user (
State (state): State<Arc<AppState>>,
Path (user_id): Path<i32 >,
Json (req): Json<UpdateUserRequest>,
) -> Result <impl IntoResponse , AppError> {
let user = sqlx::query_as!(
User,
"SELECT * FROM users WHERE id = $1" ,
user_id
)
.fetch_one (&state.db_pool)
.await ?;
let name = req.name.unwrap_or (user.name);
let age = req.age.unwrap_or (user.age);
let email = req.email.unwrap_or (user.email);
let updated_user = sqlx::query_as!(
User,
"UPDATE users SET name = $1, age = $2, email = $3 WHERE id = $4 RETURNING *" ,
name,
age,
email,
user_id
)
.fetch_one (&state.db_pool)
.await ?;
Ok (Json (updated_user))
}
async fn delete_user (
State (state): State<Arc<AppState>>,
Path (user_id): Path<i32 >,
) -> Result <impl IntoResponse , AppError> {
let result = sqlx::query!("DELETE FROM users WHERE id = $1" , user_id)
.execute (&state.db_pool)
.await ?;
if result.rows_affected () == 0 {
return Err (AppError::NotFound);
}
Ok (StatusCode::NO_CONTENT)
}
#[tokio::main]
async fn main () {
dotenv ().ok ();
tracing_subscriber::fmt::init ();
let database_url = env::var ("DATABASE_URL" ).expect ("DATABASE_URL 未设置" );
let db_pool = PgPoolOptions::new ()
.max_connections (5 )
.connect (&database_url)
.await
.expect ("无法连接到数据库" );
let app_state = Arc::new (AppState { db_pool });
let app = Router::new ()
.route ("/users" , post (create_user))
.route ("/users" , get (list_users))
.route ("/users/:id" , get (get_user))
.route ("/users/:id" , put (update_user))
.route ("/users/:id" , delete (delete_user))
.with_state (app_state);
let addr = SocketAddr::from (([0 , 0 , 0 , 0 ], 3000 ));
println! ("服务器运行在 http://{}" , addr);
Server::bind (&addr).serve (app.into_make_service ()).await .unwrap ();
}
测试接口 curl -X POST -H "Content-Type: application/json" -d '{"name":"张三","age":25,"email":"[email protected] "}' http://localhost:3000/users
curl http://localhost:3000/users?page=1&size=10
curl http://localhost:3000/users/1
curl -X PUT -H "Content-Type: application/json" -d '{"age":26}' http://localhost:3000/users/1
curl -X DELETE http://localhost:3000/users/1
周边工具与优化 生态这块,身份认证可以用 axum-login 或 jsonwebtoken;自动生成 OpenAPI 文档用 utoipa + utoipa-swagger-ui;缓存、限流都能在 tower-http 里找到。
性能优化上几个要点:高并发 I/O 考虑启用 tokio-uring;数据库连接池别开太大,一般等于 CPU 核数就行;响应压缩用 CompressionLayer;别在异步任务里做阻塞操作,该用 spawn_blocking 就用;编译时记得 --release。
Axum 与 Actix-web:宏的取舍 Actix-web 是另一款主流框架。它俩最直观的区别就是:Actix 重度用宏,Axum 几乎不用。
Actix 的路由要靠 #[get]、#[post] 这些属性宏,参数提取得用 web::Path、web::Json 从上下文里拉,这些东西背后都是宏展开生成的代码。好处是写起来快,坏处是出了错只能盯着宏报错信息猜,逻辑也看不见。
Axum 的做法是:路由就是普通的函数调用,参数提取就是函数参数的类型,全是显式 Rust 语法。比如:
Router::new ().route ("/user" , get (get_user))
#[get("/user" )]
async fn get_user () -> impl Responder { ... }
async fn update_user (Path (user_id): Path<u32 >, Json (user): Json<User>) -> String { ... }
#[post("/user/{user_id}" )]
async fn update_user (user_id: web::Path<u32 >, user: web::Json<User>) -> impl Responder { ... }
Axum 的模式让你能直接读懂数据从哪里来、是什么类型,调试时直接跳进函数看,不用猜宏背后干了什么。Actix 的宏封装减少了手写代码量,但也增加了'魔法'感,学习曲线不一定低。
维度 Axum(无宏) actix-web(宏依赖) 代码可读性 贴近原生 Rust,逻辑直白 宏隐藏底层,需记忆框架专属规则 调试 类型错误在编译期直接暴露,可跳转函数 宏展开后复杂,错误提示指向宏内部 灵活性 可用普通 Rust 逻辑动态调整路由 宏逻辑固定,动态调整受限 学习成本 掌握 Rust 类型系统和异步即可 需额外学习框架自定义宏
Axum 的思路是把框架能力融进 Rust 的 trait 和类型系统,而不是另外发明一套语法。这正好和'写 Web 服务就是写普通 Rust 程序'的理念对上了。
一点体会 Axum 让我觉得踏实的地方就是:你写的代码就是普通的 Rust 函数,路由、提取器全用类型组合表达,不用猜测宏背后生成了什么。配上 SQLx 做数据库操作,开发体验很连贯。异步那一套只要熟悉 Tokio,基本没什么障碍。如果你已经在用 Rust 做后端,Axum 值得花时间深入。
相关免费在线工具 Base64 字符串编码/解码 将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
Base64 文件转换器 将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
Markdown转HTML 将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
HTML转Markdown 将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
JSON 压缩 通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
JSON美化和格式化 将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online