第六章:RESTful API 设计
博客v1.0系列教程(Rust)博客 v1.0 系列教程 (Rust)
6.1 API 路由组织
// src/api/article.rs
use axum::{
extract::{Path, Query, State},
routing::{delete, get, post, put},
Json, Router,
};
use sqlx::PgPool;
pub fn routes() -> Router<PgPool> {
Router::new()
.route("/api/article", get(list_articles))
.route("/api/article/{id}", get(get_article))
.route("/api/article", post(create_article))
.route("/api/article", put(update_article))
.route("/api/article/{id}", delete(delete_article))
.route("/api/article/{id}/digg", post(toggle_digg))
.route("/api/article/history", get(get_history))
.route("/api/article/history", post(record_history))
.route("/api/article/tag/options", get(get_tag_options))
}
6.2 文章端点实现
// 列表 + 分页 + 筛选
async fn list_articles(
State(pool): State<PgPool>,
Query(params): Query<ArticleQuery>,
) -> Result<Json<PageResult<Article>>, AppError> {
let page = params.page.unwrap_or(1);
let page_size = params.page_size.unwrap_or(10);
let offset = (page - 1) * page_size;
let mut where_clauses = Vec::new();
let mut bind_params: Vec<String> = Vec::new();
if let Some(ref cat) = params.category {
let idx = bind_params.len() + 1;
where_clauses.push(format!("category = ${}", idx));
bind_params.push(cat.clone());
}
if let Some(ref kw) = params.keyword {
let idx = bind_params.len() + 1;
where_clauses.push(format!("title ILIKE ${}", idx));
bind_params.push(format!("%{}%", kw));
}
let where_sql = if where_clauses.is_empty() {
String::new()
} else {
format!("WHERE {}", where_clauses.join(" AND "))
};
// 计数查询
let count_sql = format!("SELECT COUNT(*) FROM article_models {}", where_sql);
let total = sqlx::query_scalar::<_, i64>(&count_sql)
.fetch_one(&pool)
.await?;
// 数据查询
let query_sql = format!(
"SELECT * FROM article_models {} ORDER BY create_time DESC LIMIT ${} OFFSET ${}",
where_sql,
bind_params.len() + 1,
bind_params.len() + 2
);
Ok(Json(PageResult {
items: Vec::new(), // 实际执行查询
total: total as u64,
page,
page_size,
}))
}
// 文章详情(含上下篇导航)
async fn get_article(
State(pool): State<PgPool>,
Path(id): Path<i64>,
) -> Result<Json<ArticleDetail>, AppError> {
let article = sqlx::query_as::<_, Article>(
"SELECT * FROM article_models WHERE id = $1"
)
.bind(id)
.fetch_optional(&pool)
.await?
.ok_or_else(|| AppError::NotFound("Article not found".into()))?;
// 获取上一篇和下一篇
let prev = sqlx::query_as::<_, Article>(
"SELECT * FROM article_models WHERE id < $1 ORDER BY id DESC LIMIT 1"
)
.bind(id)
.fetch_optional(&pool)
.await?;
let next = sqlx::query_as::<_, Article>(
"SELECT * FROM article_models WHERE id > $1 ORDER BY id ASC LIMIT 1"
)
.bind(id)
.fetch_optional(&pool)
.await?;
Ok(Json(ArticleDetail { article, prev, next }))
}
6.3 评论端点
// src/api/comment.rs
pub fn routes() -> Router<PgPool> {
Router::new()
.route("/api/comment", get(list_comments))
.route("/api/comment", post(create_comment))
.route("/api/comment/{id}", delete(delete_comment))
.route("/api/comment/tree/{article_id}", get(get_comment_tree))
}
async fn create_comment(
State(pool): State<PgPool>,
Json(dto): Json<CreateCommentDto>,
) -> Result<Json<Comment>, AppError> {
let comment = sqlx::query_as::<_, Comment>(
"INSERT INTO comment_models (content, article_id, user_id, parent_id, root_parent_id)
VALUES ($1, $2, $3, $4, COALESCE($5, $4))
RETURNING *"
)
.bind(&dto.content)
.bind(dto.article_id)
.bind(dto.user_id)
.bind(dto.parent_id)
.bind(dto.root_parent_id)
.fetch_one(&pool)
.await?;
Ok(Json(comment))
}
6.4 分类端点
// src/api/category.rs
pub fn routes() -> Router<PgPool> {
Router::new()
.route("/api/category", get(list_categories))
.route("/api/category/options", get(get_category_options))
.route("/api/category", post(create_category))
.route("/api/category", delete(delete_category))
}
async fn list_categories(
State(pool): State<PgPool>,
) -> Result<Json<Vec<Category>>, AppError> {
let categories = sqlx::query_as::<_, Category>(
"SELECT * FROM category_models ORDER BY name"
)
.fetch_all(&pool)
.await?;
Ok(Json(categories))
}
6.5 路由聚合
// src/main.rs
fn create_router(pool: PgPool, config: AppConfig) -> Router {
let api_routes = Router::new()
.merge(api::article::routes())
.merge(api::category::routes())
.merge(api::comment::routes())
.merge(api::user::routes())
.merge(api::site::routes())
.merge(api::upload::routes())
.merge(api::data::routes())
.with_state(pool.clone());
Router::new()
.merge(api_routes)
.layer(middleware::from_fn_with_state(
config.clone(),
auth_middleware,
))
.layer(CorsLayer::permissive())
.with_state(pool)
}
下一章将深入错误处理与统一响应。
rustrestapiaxumrouting