端点与模型
路由与数据
端点与模型 是 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 反馈 — 无需本地设置。