エンドポイントとモデル
ルートとデータ
「エンドポイントとモデル」はCoddyKit上の無料Learn Rust Codingレッスンです。 これはレッスン2/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはLearn Rust Coding学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 Learn Rust Codingコースには全4レッスンが含まれています。
ルートとデータモデル
APIは、エンドポイント(URLとHTTPメソッドの組み合わせ)と、そこを流れるモデル(データの形)によって定義されます。このレッスンでは、serdeを使ってリクエストとレスポンスのモデルを定義し、AxumでCRUD形式のルートを接続します。
モデルを定義する
モデルは単純なRust構造体です。レスポンスでJSONに変換できるようにSerializeを導出し、リクエストボディから解析できるように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))
}パスパラメーター
1つのリソースを取得するには、パスパラメーターを使って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エラーを返せます。
よくある質問
「エンドポイントとモデル」レッスンは無料ですか?
はい。「エンドポイントとモデル」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、Learn Rust Codingコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 Learn Rust Codingコースには全4レッスンが含まれています。
「エンドポイントとモデル」で何を学びますか?
ルートとデータ ブラウザで直接実行するハンズオンコードでLearn Rust Codingを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。
Learn Rust Codingを始めるのに経験は必要ですか?
事前経験は必要ありません。CoddyKitのLearn Rust Codingは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン2/4です。
「エンドポイントとモデル」レッスンにはどのくらい時間がかかりますか?
ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。
このLearn Rust Codingレッスンでコードを書いて実行できますか?
はい。すべてのLearn Rust Codingレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。
このコースのすべてのレッスン
- プロジェクトのセットアップ
- エンドポイントとモデル
- データベース統合
- APIのテスト