上一章
CH.125 小时实战: 博客 REST API

Web 后端

用 Axum 构建生产级 REST API

为什么选 Axum?

Rust Web 框架有多个选择:Actix-web(极致性能)、Rocket(宏魔法)、Warp(函数式)。 Axum 是 Tokio 团队官方出品,与 Tokio 生态无缝集成,API 设计接近 Express/Hono,是最适合前端工程师的切入点。

# Cargo.toml
[dependencies]
axum = "0.8"
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tower = "0.5"
tower-http = { version = "0.6", features = ["cors", "trace"] }

Hello World:最简 Axum 服务

对比:基础 HTTP 服务器
JS
Express.js
import express from 'express';
const app = express();
app.use(express.json());

app.get('/', (req, res) => {
res.json({ message: 'Hello World' });
});

app.listen(3000, () => {
console.log('Server on port 3000');
});
Rs
Axum
use axum::{routing::get, Json, Router};
use serde_json::{json, Value};

async fn root() -> Json<Value> {
  Json(json!({ "message": "Hello World" }))
}

#[tokio::main]
async fn main() {
  let app = Router::new().route("/", get(root));

  let listener = tokio::net::TcpListener::bind("0.0.0.0:3000")
      .await.unwrap();
  println!("Server on port 3000");
  axum::serve(listener, app).await.unwrap();
}

结构几乎一致:Router → 路由 → Handler。区别是 Axum 的 handler 是普通 async fn,参数通过提取器(Extractor)注入,比 Express 的 (req, res) 更类型安全。


路由系统

use axum::{
    routing::{get, post, put, delete},
    Router,
};
 
// 路由嵌套,类似 Express Router
pub fn posts_router() -> Router {
    Router::new()
        .route("/posts",          get(list_posts).post(create_post))
        .route("/posts/:id",      get(get_post).put(update_post).delete(delete_post))
}
 
// 组合多个路由
let app = Router::new()
    .nest("/api/v1", posts_router())
    .nest("/api/v1", users_router())
    .route("/health", get(|| async { "ok" }));

提取器(Extractors):类型安全的参数注入

Axum 最优雅的设计——handler 的参数自动从请求中提取,编译期保证类型正确:

use axum::{
    extract::{Path, Query, State, Json},
    http::StatusCode,
};
use serde::{Deserialize, Serialize};
 
// 路径参数
async fn get_post(Path(id): Path<u32>) -> String {
    format!("Post #{id}")
}
 
// 查询参数
#[derive(Deserialize)]
struct Pagination { page: Option<u32>, per_page: Option<u32> }
 
async fn list_posts(Query(params): Query<Pagination>) -> String {
    format!("Page {:?}, size {:?}", params.page, params.per_page)
}
 
// JSON 请求体
#[derive(Deserialize)]
struct CreatePost { title: String, content: String }
 
#[derive(Serialize)]
struct Post { id: u32, title: String, content: String }
 
async fn create_post(
    Json(body): Json<CreatePost>,
) -> (StatusCode, Json<Post>) {
    let post = Post { id: 1, title: body.title, content: body.content };
    (StatusCode::CREATED, Json(post))
}

多个提取器:handler 可以有多个参数,每个都是一种提取器。Axum 按顺序提取,只要不重复消费 body(JSON body 只能提取一次)就可以自由组合。


共享状态:AppState

Web 服务通常需要共享数据库连接池、配置等。Axum 用 State 提取器优雅处理:

use axum::extract::State;
use std::sync::Arc;
use tokio::sync::RwLock;
 
// 应用状态(将来换成数据库连接池)
#[derive(Clone)]
struct AppState {
    posts: Arc<RwLock<Vec<Post>>>,
}
 
#[tokio::main]
async fn main() {
    let state = AppState {
        posts: Arc::new(RwLock::new(Vec::new())),
    };
 
    let app = Router::new()
        .route("/posts", get(list_posts).post(create_post))
        .with_state(state);  // 注入状态
 
    // ...
}
 
async fn list_posts(
    State(state): State<AppState>,  // 自动注入
) -> Json<Vec<Post>> {
    let posts = state.posts.read().await;
    Json(posts.clone())
}
 
async fn create_post(
    State(state): State<AppState>,
    Json(body): Json<CreatePost>,
) -> (StatusCode, Json<Post>) {
    let post = Post { id: 1, title: body.title, content: body.content };
    state.posts.write().await.push(post.clone());
    (StatusCode::CREATED, Json(post))
}

错误处理:统一的 AppError

Rust 的错误处理哲学:错误是值,不是异常。在 Web 服务中,统一错误类型让代码更干净:

use axum::{
    http::StatusCode,
    response::{IntoResponse, Response},
    Json,
};
use serde_json::json;
 
// 统一错误类型
pub enum AppError {
    NotFound(String),
    Unauthorized,
    BadRequest(String),
    Internal(String),
}
 
// 转换为 HTTP 响应
impl IntoResponse for AppError {
    fn into_response(self) -> Response {
        let (status, message) = match self {
            AppError::NotFound(msg)   => (StatusCode::NOT_FOUND, msg),
            AppError::Unauthorized    => (StatusCode::UNAUTHORIZED, "未授权".into()),
            AppError::BadRequest(msg) => (StatusCode::BAD_REQUEST, msg),
            AppError::Internal(msg)   => (StatusCode::INTERNAL_SERVER_ERROR, msg),
        };
 
        (status, Json(json!({ "error": message }))).into_response()
    }
}
 
// handler 直接返回 Result,错误自动转换
async fn get_post(Path(id): Path<u32>) -> Result<Json<Post>, AppError> {
    let post = find_post(id)
        .ok_or_else(|| AppError::NotFound(format!("Post #{id} 不存在")))?;
    Ok(Json(post))
}

中间件:Tower Layer

Axum 的中间件基于 tower,和 Express middleware 概念一致:

use tower_http::{cors::CorsLayer, trace::TraceLayer};
use axum::middleware;
 
// 内置中间件:CORS + 日志
let app = Router::new()
    .route("/posts", get(list_posts))
    .layer(CorsLayer::permissive())    // CORS
    .layer(TraceLayer::new_for_http()) // 请求日志
 
// 自定义中间件:JWT 验证
async fn auth_middleware(
    req: axum::extract::Request,
    next: axum::middleware::Next,
) -> Result<Response, AppError> {
    let token = req
        .headers()
        .get("Authorization")
        .and_then(|v| v.to_str().ok())
        .and_then(|v| v.strip_prefix("Bearer "))
        .ok_or(AppError::Unauthorized)?;
 
    verify_jwt(token).map_err(|_| AppError::Unauthorized)?;
    Ok(next.run(req).await)
}
 
// 只对特定路由应用验证
let app = Router::new()
    .route("/posts", get(list_posts))
    .route("/posts", post(create_post)
        .route_layer(middleware::from_fn(auth_middleware))); // 仅 POST 需要鉴权

实战项目:博客 REST API

实战项目

博客 REST API

90 分钟中级

用 Axum 构建完整的博客后端,包含文章 CRUD、标签过滤、JWT 认证,内存存储(下一章接数据库)。

Axum RouterExtractorAppStateAppErrorCORS 中间件JWT 基础
cargo new blog-api && cd blog-api
// 完整 API 结构预览
// GET    /api/posts          — 文章列表(支持 ?tag=rust 过滤)
// POST   /api/posts          — 创建文章(需要 JWT)
// GET    /api/posts/:id      — 文章详情
// PUT    /api/posts/:id      — 更新文章(需要 JWT)
// DELETE /api/posts/:id      — 删除文章(需要 JWT)
// POST   /api/auth/login     — 登录,返回 JWT token
 
use axum::{extract::{Path, Query, State}, routing::{get, post}, Json, Router};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use tokio::sync::RwLock;
 
#[derive(Debug, Clone, Serialize, Deserialize)]
struct Post {
    id:      u32,
    title:   String,
    content: String,
    tags:    Vec<String>,
    author:  String,
}
 
#[derive(Clone)]
struct AppState {
    posts:   Arc<RwLock<Vec<Post>>>,
    next_id: Arc<RwLock<u32>>,
}
 
impl AppState {
    fn new() -> Self {
        Self {
            posts:   Arc::new(RwLock::new(Vec::new())),
            next_id: Arc::new(RwLock::new(1)),
        }
    }
}
 
#[tokio::main]
async fn main() {
    let state = AppState::new();
 
    let app = Router::new()
        .route("/api/posts",     get(list_posts).post(create_post))
        .route("/api/posts/:id", get(get_post))
        .with_state(state);
 
    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
    println!("博客 API 启动于 http://localhost:3000");
    axum::serve(listener, app).await.unwrap();
}
 
#[derive(Deserialize)]
struct ListQuery { tag: Option<String> }
 
async fn list_posts(
    State(s): State<AppState>,
    Query(q): Query<ListQuery>,
) -> Json<Vec<Post>> {
    let posts = s.posts.read().await;
    let result: Vec<Post> = match &q.tag {
        Some(tag) => posts.iter().filter(|p| p.tags.contains(tag)).cloned().collect(),
        None      => posts.clone(),
    };
    Json(result)
}
 
async fn get_post(
    State(s): State<AppState>,
    Path(id): Path<u32>,
) -> Result<Json<Post>, StatusCode> {
    let posts = s.posts.read().await;
    posts.iter()
        .find(|p| p.id == id)
        .cloned()
        .map(Json)
        .ok_or(StatusCode::NOT_FOUND)
}
 
#[derive(Deserialize)]
struct CreatePostBody { title: String, content: String, tags: Vec<String> }
 
async fn create_post(
    State(s): State<AppState>,
    Json(body): Json<CreatePostBody>,
) -> (StatusCode, Json<Post>) {
    let mut next_id = s.next_id.write().await;
    let post = Post {
        id:      *next_id,
        title:   body.title,
        content: body.content,
        tags:    body.tags,
        author:  "admin".to_string(),
    };
    *next_id += 1;
    s.posts.write().await.push(post.clone());
    (StatusCode::CREATED, Json(post))
}

实战视频

Axum Web 后端:REST API 实战
56 秒精讲
0:00 / 0:00
CC
Axum 路由、提取器、错误处理、中间件。

本章小结

  • ✓ Axum 路由系统与 Express 的对比
  • ✓ 提取器:Path / Query / Json / State
  • ✓ 统一错误类型 AppError + IntoResponse
  • ✓ Tower 中间件:CORS、日志、自定义鉴权
  • ✓ 内存状态 AppState(Arc + RwLock)

下一章:数据库与持久化——把内存状态换成真正的 PostgreSQL,用 SQLx 实现类型安全查询。