用Axum构建Web API —— 从零到一搭建后端服务
学了这么多Rust基础知识,是时候用它做点”看得见、摸得着”的东西了。在这一章里,我们要用Axum框架从零开始搭建一个RESTful API——一个简单但完整的待办事项(Todo)服务。你会看到Rust在Web开发领域是多么高效和优雅。
📋 开篇自测:你已经知道多少?
- 你知道RESTful API的基本概念吗?GET、POST、PUT、DELETE分别用于什么?
- 你了解HTTP请求和响应的基本结构吗?
- 你用过其他语言的Web框架吗(如Express、Flask、Spring Boot)?
一、为什么选Axum?
Rust生态中有几个主流的Web框架:
| 框架 | 特点 |
|---|---|
| Actix-web | 老牌框架,性能极高,功能丰富 |
| Axum | Tokio团队出品,设计现代,和Tokio生态深度集成 |
| Rocket | 对开发者最友好,API设计优雅 |
| Warp | 基于Filter组合的函数式风格 |
我们选择Axum,因为:
- 它由Tokio团队维护,和Rust异步生态完美兼容
- 设计理念现代,利用Rust的类型系统实现零成本抽象
- 错误信息友好,新手容易上手
- 社区活跃,更新迅速
二、项目初始化——搭建骨架
2.1 创建项目
cargo new todo_api
cd todo_api
2.2 添加依赖
编辑Cargo.toml:
[package]
name = "todo_api"
version = "0.1.0"
edition = "2024"
[dependencies]
axum = "0.8"
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
uuid = { version = "1", features = ["v4"] }
tower-http = { version = "0.6", features = ["cors"] }
tracing = "0.1"
tracing-subscriber = "0.3"
各依赖的作用:
- axum:Web框架本体
- tokio:异步运行时
- serde:序列化/反序列化(把Rust结构体和JSON互转)
- uuid:生成唯一ID
- tower-http:HTTP中间件(跨域支持等)
- tracing:日志系统
2.3 最简单的Hello World服务器
use axum::{routing::get, Router};
async fn hello() -> &'static str {
"Hello, Axum!"
}
#[tokio::main]
async fn main() {
let app = Router::new().route("/", get(hello));
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000")
.await
.unwrap();
println!("服务器启动在 http://localhost:3000");
axum::serve(listener, app).await.unwrap();
}
运行cargo run,在浏览器打开http://localhost:3000,你就能看到”Hello, Axum!”。三十行代码,一个Web服务器就跑起来了!
🤔 想一想 Axum的handler函数就是一个普通的async函数。和Spring Boot的
@GetMapping注解方式相比,哪种设计你更喜欢?各有什么优缺点?
三、设计Todo API——数据模型与路由
3.1 定义数据模型
use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Todo {
pub id: String,
pub title: String,
pub description: Option<String>,
pub completed: bool,
}
#[derive(Debug, Deserialize)]
pub struct CreateTodoRequest {
pub title: String,
pub description: Option<String>,
}
#[derive(Debug, Deserialize)]
pub struct UpdateTodoRequest {
pub title: Option<String>,
pub description: Option<String>,
pub completed: Option<bool>,
}
impl Todo {
pub fn new(title: String, description: Option<String>) -> Self {
Todo {
id: Uuid::new_v4().to_string(),
title,
description,
completed: false,
}
}
}
#[derive(Serialize, Deserialize)]让serde自动帮我们处理JSON的序列化和反序列化——再也不用手动解析JSON了。
3.2 应用状态管理
我们用内存存储来简化,用Arc<Mutex<Vec<Todo>>>在多个请求之间共享数据:
use std::sync::{Arc, Mutex};
// 这里使用 std::sync::Mutex 而非 tokio::sync::Mutex。
// 因为我们的 handler 在持有锁期间不会跨越 .await 点(锁在同一个代码块内获取和释放),
// 所以 std::sync::Mutex 就够了,而且它不需要 .await 来获取锁,开销更小。
// 如果你需要在持有锁的同时做异步操作(如数据库查询),则应改用 tokio::sync::Mutex。
type TodoStore = Arc<Mutex<Vec<Todo>>>;
fn create_store() -> TodoStore {
Arc::new(Mutex::new(Vec::new()))
}
3.3 设计路由
use axum::{routing::get, Router};
fn create_router(store: TodoStore) -> Router {
Router::new()
.route("/todos", get(list_todos).post(create_todo))
.route(
"/todos/{id}",
get(get_todo).put(update_todo).delete(delete_todo),
)
.with_state(store)
}
// 注意:这里只导入了 routing::get,而 .post()、.put()、.delete() 是
// MethodRouter 上的链式方法,不需要单独导入。如果你偏好每种方法独立定义路由,
// 也可以 use axum::routing::{get, post, put, delete} 然后用 post(handler) 等。
REST风格的路由设计:
GET /todos→ 获取所有待办POST /todos→ 创建新待办GET /todos/{id}→ 获取指定待办PUT /todos/{id}→ 更新指定待办DELETE /todos/{id}→ 删除指定待办
四、实现Handler——业务逻辑
4.1 获取所有待办
use axum::{
extract::{Path, State},
http::StatusCode,
Json,
};
async fn list_todos(
State(store): State<TodoStore>,
) -> Json<Vec<Todo>> {
let todos = store.lock().unwrap();
Json(todos.clone())
}
Axum的”提取器”(Extractor)是一个很巧妙的设计——通过函数参数的类型来自动提取请求中的数据。State(store)自动从应用状态中提取TodoStore,Json(...)自动把返回值序列化为JSON。
4.2 创建待办
async fn create_todo(
State(store): State<TodoStore>,
Json(input): Json<CreateTodoRequest>,
) -> (StatusCode, Json<Todo>) {
let todo = Todo::new(input.title, input.description);
let mut todos = store.lock().unwrap();
todos.push(todo.clone());
(StatusCode::CREATED, Json(todo))
}
Json(input)会自动把请求体的JSON反序列化为CreateTodoRequest结构体。如果JSON格式不对,Axum会自动返回400错误。
4.3 获取单个待办
async fn get_todo(
State(store): State<TodoStore>,
Path(id): Path<String>,
) -> Result<Json<Todo>, StatusCode> {
let todos = store.lock().unwrap();
match todos.iter().find(|t| t.id == id) {
Some(todo) => Ok(Json(todo.clone())),
None => Err(StatusCode::NOT_FOUND),
}
}
Path(id)从URL路径中提取参数。返回Result让我们可以返回不同的HTTP状态码。
4.4 更新待办
async fn update_todo(
State(store): State<TodoStore>,
Path(id): Path<String>,
Json(input): Json<UpdateTodoRequest>,
) -> Result<Json<Todo>, StatusCode> {
let mut todos = store.lock().unwrap();
match todos.iter_mut().find(|t| t.id == id) {
Some(todo) => {
if let Some(title) = input.title {
todo.title = title;
}
if let Some(desc) = input.description {
todo.description = Some(desc);
}
if let Some(completed) = input.completed {
todo.completed = completed;
}
Ok(Json(todo.clone()))
}
None => Err(StatusCode::NOT_FOUND),
}
}
4.5 删除待办
async fn delete_todo(
State(store): State<TodoStore>,
Path(id): Path<String>,
) -> StatusCode {
let mut todos = store.lock().unwrap();
let len_before = todos.len();
todos.retain(|t| t.id != id);
if todos.len() < len_before {
StatusCode::NO_CONTENT
} else {
StatusCode::NOT_FOUND
}
}
五、完整代码与运行
5.1 完整的main.rs
use axum::{
extract::{Path, State},
http::StatusCode,
routing::get,
Json, Router,
};
use serde::{Deserialize, Serialize};
use std::sync::{Arc, Mutex};
use uuid::Uuid;
// ===== 数据模型 =====
#[derive(Debug, Clone, Serialize, Deserialize)]
struct Todo {
id: String,
title: String,
description: Option<String>,
completed: bool,
}
#[derive(Debug, Deserialize)]
struct CreateTodoRequest {
title: String,
description: Option<String>,
}
#[derive(Debug, Deserialize)]
struct UpdateTodoRequest {
title: Option<String>,
description: Option<String>,
completed: Option<bool>,
}
impl Todo {
fn new(title: String, description: Option<String>) -> Self {
Todo {
id: Uuid::new_v4().to_string(),
title,
description,
completed: false,
}
}
}
type TodoStore = Arc<Mutex<Vec<Todo>>>;
// ===== Handler函数 =====
async fn list_todos(State(store): State<TodoStore>) -> Json<Vec<Todo>> {
let todos = store.lock().unwrap();
Json(todos.clone())
}
async fn create_todo(
State(store): State<TodoStore>,
Json(input): Json<CreateTodoRequest>,
) -> (StatusCode, Json<Todo>) {
let todo = Todo::new(input.title, input.description);
let mut todos = store.lock().unwrap();
todos.push(todo.clone());
(StatusCode::CREATED, Json(todo))
}
async fn get_todo(
State(store): State<TodoStore>,
Path(id): Path<String>,
) -> Result<Json<Todo>, StatusCode> {
let todos = store.lock().unwrap();
todos
.iter()
.find(|t| t.id == id)
.cloned()
.map(Json)
.ok_or(StatusCode::NOT_FOUND)
}
async fn update_todo(
State(store): State<TodoStore>,
Path(id): Path<String>,
Json(input): Json<UpdateTodoRequest>,
) -> Result<Json<Todo>, StatusCode> {
let mut todos = store.lock().unwrap();
match todos.iter_mut().find(|t| t.id == id) {
Some(todo) => {
if let Some(title) = input.title {
todo.title = title;
}
if let Some(desc) = input.description {
todo.description = Some(desc);
}
if let Some(completed) = input.completed {
todo.completed = completed;
}
Ok(Json(todo.clone()))
}
None => Err(StatusCode::NOT_FOUND),
}
}
async fn delete_todo(
State(store): State<TodoStore>,
Path(id): Path<String>,
) -> StatusCode {
let mut todos = store.lock().unwrap();
let len_before = todos.len();
todos.retain(|t| t.id != id);
if todos.len() < len_before {
StatusCode::NO_CONTENT
} else {
StatusCode::NOT_FOUND
}
}
async fn health_check() -> &'static str {
"OK"
}
// ===== 主函数 =====
#[tokio::main]
async fn main() {
// 初始化日志
tracing_subscriber::fmt::init();
let store: TodoStore = Arc::new(Mutex::new(Vec::new()));
let app = Router::new()
.route("/health", get(health_check))
.route("/todos", get(list_todos).post(create_todo))
.route(
"/todos/{id}",
get(get_todo).put(update_todo).delete(delete_todo),
)
.with_state(store);
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000")
.await
.unwrap();
tracing::info!("服务器启动在 http://localhost:3000");
axum::serve(listener, app).await.unwrap();
}
5.2 测试API
启动服务器后,用curl测试:
# 健康检查
curl http://localhost:3000/health
# 创建待办
curl -X POST http://localhost:3000/todos \
-H "Content-Type: application/json" \
-d '{"title":"学习Rust","description":"完成前三章"}'
# 获取所有待办
curl http://localhost:3000/todos
# 更新待办(替换{id}为实际的ID)
curl -X PUT http://localhost:3000/todos/{id} \
-H "Content-Type: application/json" \
-d '{"completed":true}'
# 删除待办
curl -X DELETE http://localhost:3000/todos/{id}
🤔 想一想 我们用
Arc<Mutex<Vec<Todo>>>做内存存储。在生产环境中,这有什么问题?(提示:服务器重启后数据就没了,而且Mutex在高并发下可能成为瓶颈。)
六、添加中间件与错误处理
6.1 添加CORS支持
use tower_http::cors::{CorsLayer, Any};
// 注意:allow_origin(Any) 仅适用于开发环境。
// 生产环境应指定允许的域名,如 .allow_origin("https://yourdomain.com".parse().unwrap())
let cors = CorsLayer::new()
.allow_origin(Any)
.allow_methods(Any)
.allow_headers(Any);
let app = Router::new()
// ... routes ...
.layer(cors)
.with_state(store);
6.2 统一错误处理
use axum::response::{IntoResponse, Response};
#[derive(Debug)]
enum ApiError {
NotFound(String),
BadRequest(String),
InternalError(String),
}
impl IntoResponse for ApiError {
fn into_response(self) -> Response {
let (status, message) = match self {
ApiError::NotFound(msg) => (StatusCode::NOT_FOUND, msg),
ApiError::BadRequest(msg) => (StatusCode::BAD_REQUEST, msg),
ApiError::InternalError(msg) => {
(StatusCode::INTERNAL_SERVER_ERROR, msg)
}
};
let body = serde_json::json!({
"error": message
});
(status, Json(body)).into_response()
}
}
这样你的handler可以返回Result<Json<T>, ApiError>,错误会自动转换为格式统一的JSON响应。
⚠️ 常见误区
- 在handler中长时间持有Mutex锁——Mutex的lock()返回的MutexGuard跨越了await点会导致问题(MutexGuard不是Send的)。解决方案:在一个代码块内完成所有锁操作,或者使用
tokio::sync::Mutex。- 忘了处理JSON解析错误——当客户端发送格式错误的JSON时,Axum默认返回的错误信息不够友好。可以自定义JSON提取器来改善。
- 不加健康检查端点——生产环境中的服务都需要一个
/health端点,让负载均衡器和监控系统检查服务是否正常。- 在生产环境使用内存存储——内存存储仅适合原型开发。下一章我们会讲如何接入真正的数据库。
📝 掌握度自测
- 基础概念:Axum的”提取器”(Extractor)是什么?
State、Path、Json各提取什么数据? - 路由设计:如果要添加一个”按标题搜索待办”的功能(
GET /todos/search?keyword=xxx),路由和handler该怎么写? - 状态管理:为什么需要用
Arc包裹Mutex?如果不用Arc会怎样? - 错误处理:handler返回
Result<Json<Todo>, StatusCode>时,不同的StatusCode会导致什么HTTP响应? - 实践扩展:给Todo添加一个
created_at字段(使用chrono库),在创建时自动记录时间。
💡 自我评估
- 答对5题:你已经具备用Rust构建Web API的能力了。
- 答对3-4题:核心流程已经掌握,建议动手把完整代码跑起来,用curl或Postman测试。
- 答对0-2题:Web开发涉及多个概念的交叉应用。建议先把完整代码跑通,然后逐个handler理解它的工作方式。
购买课程解锁全部内容
内存安全 + 零成本抽象:Rust 系统编程实战
¥29.90