第六章: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