0Pricing
Learn Rust Coding · 课时

端点与模型

路由与数据

端点与模型 是 CoddyKit 上的免费 Learn Rust Coding 课时。 这是第 2 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 Learn Rust Coding 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 Learn Rust Coding 课程共包含 4 节课。

路由和数据模型

API 由端点(URL 和 HTTP 方法)以及在其中传递的模型(数据结构)定义。本课将使用 serde 定义请求和响应模型,并在 Axum 中连接 CRUD 风格的路由。

定义模型

模型就是一个普通的 Rust 结构体。派生 Serialize 后,它可以在响应中转换为 JSON;派生 Deserialize 后,它可以从请求正文中解析出来。字段名称会直接映射为 JSON 键。

use serde::{Serialize, Deserialize};

#[derive(Serialize, Deserialize, Clone)]
struct Todo {
    id: u32,
    title: String,
    done: bool,
}

分离请求类型和响应类型

客户端创建资源时不应发送由服务器分配的 id。请为请求正文使用单独的输入结构体,并在响应中使用完整模型。这样可以使约定更加清晰,也能防止客户端设置不应由其设置的字段。

use serde::Deserialize;

#[derive(Deserialize)]
struct CreateTodo {
    title: String,
}
// The handler assigns the id and sets done = false.

GET:列出资源

GET /todos 处理函数会以 JSON 数组形式返回整个集合。它会读取共享状态,快速将数据克隆到锁外,然后将其封装在 Json 中。

use axum::{extract::State, Json};
use std::sync::{Arc, Mutex};

type Store = Arc<Mutex<Vec<Todo>>>;
#[derive(Clone, serde::Serialize)]
struct Todo { id: u32, title: String, done: bool }

async fn list_todos(State(store): State<Store>) -> Json<Vec<Todo>> {
    let todos = store.lock().unwrap().clone();
    Json(todos)
}

POST:创建资源

POST /todos 处理函数使用 Json 提取器读取 JSON 正文,分配新的 id,存储项目,并以 201 Created 状态返回该项目。元组 (StatusCode, Json<T>) 允许您同时设置状态和正文。

use axum::{extract::State, Json, http::StatusCode};

async fn create_todo(
    State(store): State<Store>,
    Json(input): Json<CreateTodo>,
) -> (StatusCode, Json<Todo>) {
    let mut todos = store.lock().unwrap();
    let id = todos.len() as u32 + 1;
    let todo = Todo { id, title: input.title, done: false };
    todos.push(todo.clone());
    (StatusCode::CREATED, Json(todo))
}

路径参数

要获取单个资源,请使用路径参数捕获 URL 的一部分。在路由中将其声明为 /todos/{id},然后使用 Path 提取器提取。您指定的类型(此处为 u32)会自动进行解析。

use axum::extract::{Path, State};
use axum::{Json, http::StatusCode};

async fn get_todo(
    State(store): State<Store>,
    Path(id): Path<u32>,
) -> Result<Json<Todo>, StatusCode> {
    let todos = store.lock().unwrap();
    match todos.iter().find(|t| t.id == id) {
        Some(t) => Ok(Json(t.clone())),
        None => Err(StatusCode::NOT_FOUND),
    }
}

查询参数

筛选和分页使用查询字符串,例如 /todos?done=true。请使用 Query 提取器,将其提取到一个实现 Deserialize 的结构体中。可选字段使用 Option,这样即使缺少参数也不会出错。

use axum::extract::{Query, State};
use axum::Json;
use serde::Deserialize;

#[derive(Deserialize)]
struct Filter { done: Option<bool> }

async fn filtered(
    State(store): State<Store>,
    Query(f): Query<Filter>,
) -> Json<Vec<Todo>> {
    let todos = store.lock().unwrap();
    let out = todos.iter()
        .filter(|t| f.done.map_or(true, |d| t.done == d))
        .cloned().collect();
    Json(out)
}

PUT 和 DELETE

更新使用带有正文的 PUT /todos/{id};删除使用 DELETE /todos/{id}。两者都会根据 id 查找项目,如果项目不存在则返回 404。删除成功时通常返回 204 No Content。

use axum::extract::{Path, State};
use axum::http::StatusCode;

async fn delete_todo(
    State(store): State<Store>,
    Path(id): Path<u32>,
) -> StatusCode {
    let mut todos = store.lock().unwrap();
    let before = todos.len();
    todos.retain(|t| t.id != id);
    if todos.len() < before { StatusCode::NO_CONTENT }
    else { StatusCode::NOT_FOUND }
}

连接路由

在路由器上注册每个处理函数。将共享路径的方法分组:/todos 处理列出和创建,/todos/{id} 处理获取、更新和删除。使用 .with_state 附加共享存储。

use axum::{routing::get, Router};

fn build(store: Store) -> Router {
    Router::new()
        .route("/todos", get(list_todos).post(create_todo))
        .route("/todos/{id}",
            get(get_todo).delete(delete_todo))
        .with_state(store)
}

验证输入

永远不要信任客户端数据。请在处理函数中进行检查,数据无效时返回 400 Bad Request。这里会在存储任何内容之前拒绝空标题,从而避免坏数据进入系统。

use axum::{Json, extract::State, http::StatusCode};

async fn create_validated(
    State(store): State<Store>,
    Json(input): Json<CreateTodo>,
) -> Result<(StatusCode, Json<Todo>), StatusCode> {
    if input.title.trim().is_empty() {
        return Err(StatusCode::BAD_REQUEST);
    }
    let mut todos = store.lock().unwrap();
    let id = todos.len() as u32 + 1;
    let todo = Todo { id, title: input.title, done: false };
    todos.push(todo.clone());
    Ok((StatusCode::CREATED, Json(todo)))
}

一致的错误响应

直接返回状态码虽然可行,但生产级 API 还应返回 JSON 错误正文。一种常见做法是定义一个实现 IntoResponse 的自定义错误枚举,将每个变体映射到状态和消息。这样可以为客户端提供可预测且机器可读的错误。

use axum::response::{IntoResponse, Response};
use axum::http::StatusCode;
use axum::Json;
use serde_json::json;

enum ApiError { NotFound, BadRequest(String) }

impl IntoResponse for ApiError {
    fn into_response(self) -> Response {
        let (status, msg) = match self {
            ApiError::NotFound => (StatusCode::NOT_FOUND, "not found".to_string()),
            ApiError::BadRequest(m) => (StatusCode::BAD_REQUEST, m),
        };
        (status, Json(json!({ "error": msg }))).into_response()
    }
}

快速检查

测试您对端点和模型的理解。

总结

您定义了端点和模型:

  • 模型是派生 Serialize/Deserialize 的结构体;请使用单独的输入类型。
  • Json、Path、Query 和 State 用于提取请求数据。
  • 将 CRUD 操作映射到 GET/POST/PUT/DELETE,并使用适当的状态码。
  • 验证输入,数据错误时返回 400。
  • 实现 IntoResponse 的自定义错误类型可以提供一致的 JSON 错误。

常见问题解答

「端点与模型」课时是免费的吗?

是的 — 「端点与模型」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 Learn Rust Coding 课程的其余内容,请升级到 CoddyKit PRO。 Learn Rust Coding 课程共包含 4 节课。

「端点与模型」这节课中我会学到什么?

路由与数据 你通过在浏览器中直接运行的动手代码来练习 Learn Rust Coding,全天候 AI 导师会在你学习这节课的过程中回答你的问题。

学习 Learn Rust Coding 需要有经验吗?

无需任何先前经验。CoddyKit 上的 Learn Rust Coding 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 2 节课,共 4 节。

「端点与模型」课时需要多长时间?

大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。

我能在这节 Learn Rust Coding 课中编写并运行代码吗?

能。每节 Learn Rust Coding 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。

此课程中的所有课时

  1. 项目设置
  2. 端点与模型
  3. 数据库集成
  4. 测试 API
← 返回 Learn Rust Coding