RustとAxumで作るWeb API
このプロジェクトでは、本物のWebサーバーを作ります。小さな ToDo API です。ほかのプログラム(Webサイト、スマホアプリ、あるいはターミナルの curl)がHTTPリクエストを送ると、サーバーはJSONで答えます。ふだん使っているアプリの多くで「バックエンド」を動かしているのも、これと同じしくみです。
使うのは axum です。Tokioチームが作った、人気があって使いやすいWebフレームワークです。axumは非同期(async)のRustと serde の上に作られているので、このプロジェクトではこれまでに学んだことをたくさん組み合わせます。構造体、Result、スマートポインタ、スレッド間での状態の共有などです。ToDoリストCLIを作ったことがあれば、考え方には見覚えがあるはずです。今回は、ToDoリストがWebのアドレスの向こうにあります。
完成イメージ
Section titled “完成イメージ”完成すると、サーバーは次のリクエストに答えられるようになります。
| メソッドとパス | すること | 成功したときのステータス |
|---|---|---|
GET /todos |
ToDoをすべて一覧にする | 200 OK |
GET /todos/{id} |
ToDoを1つ取得する | 200 OK、または 404 Not Found |
POST /todos |
JSONからToDoを作る | 201 Created |
DELETE /todos/{id} |
ToDoを削除する | 204 No Content、または 404 Not Found |
ToDoはJSONでは次のような形です。
{"id":1,"title":"牛乳を買う","done":false}大切な注意:Webサーバーはリクエストをずっと待ち続け、ネットワークのポートも必要なので、Rust Playgroundでは動きません。このページのサーバーのコードは、自分のコンピュータで cargo run して動かすためのものです。以下のステップはすべて実際にコンパイルして実行したもので、載せているレスポンスも本物です。プログラムの一部をPlaygroundで動かせるところには、「Try it Yourself」ボタンを用意しています。
プロジェクトの準備
Section titled “プロジェクトの準備”新しいプロジェクトを作り、4つのクレートを追加します。
cargo new todo-apicd todo-apicargo add serde --features derivecargo add serde_jsonこれで Cargo.toml はだいたい次のようになります(バージョン番号はもっと新しいかもしれません)。
[dependencies]axum = "0.8"serde = { version = "1", features = ["derive"] }serde_json = "1"tokio = { version = "1", features = ["full"] }それぞれの役割は次のとおりです。
- axum はHTTPを担当します。リクエストを読み、レスポンスを送ります。
- tokio は、すべてを動かす非同期ランタイムです。async/awaitを参照してください。
- serde と serde_json は、構造体とJSONを相互に変換します。RustでJSONを扱うを参照してください。
リクエストを送るための curl も必要です。macOS、Linux、Windows 10以降には最初から入っています(WindowsのPowerShellでは curl.exe と入力してください。PowerShellでただ curl と書くと、別のコマンドになってしまいます)。
ステップ1: Hello, world!
Section titled “ステップ1: Hello, world!”src/main.rs を次の内容に置き換えます。
use axum::{routing::get, Router};
#[tokio::main]async fn main() { let app = Router::new().route("/", get(hello));
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap(); println!("Listening on http://127.0.0.1:3000"); axum::serve(listener, app).await.unwrap();}
async fn hello() -> &'static str { "こんにちは、世界!"}ここでは3つの部品が協力して動いています。
- ハンドラ(handler)は、リクエストに答える
async fnです。helloは文字列を返し、axumがそれをレスポンスとして送り返します。 - ルーター(router)は、パスとハンドラを結びつけます。
.route("/", get(hello))は「誰かが/にGETリクエストを送ってきたら、helloを呼ぶ」という意味です。 - リスナー(listener)は自分のコンピュータ(
127.0.0.1)の3000番ポートを開き、axum::serveがリクエストへの応答を始めます。止めるまで動き続けます。
cargo run で実行します。プログラムは1行表示して、そのまま待ち続けます。
Listening on http://127.0.0.1:3000そのまま動かしておいて、もう1つターミナルを開き、リクエストを送ってみましょう。
curl http://127.0.0.1:3000/こんにちは、世界!サーバーは文字列のあとに改行を付けないので、ターミナルのプロンプトが こんにちは、世界! のすぐ後ろに表示されるかもしれません。それで正常です。このページでは、レスポンスをそれぞれ1行ずつ表示しています。ブラウザで http://127.0.0.1:3000/ を開いてもかまいません。サーバーを止めるには、最初のターミナルで Ctrl + C を押します。コードを変更したら、そのたびにサーバーを止めて起動し直す必要があります。
ステップ2: 構造体からJSONを返す
Section titled “ステップ2: 構造体からJSONを返す”APIはたいていJSONで答えます。axumでは、serdeの Serialize を実装した値を Json(...) で包むと、axumがそれをJSONに変換し、正しい content-type ヘッダーも設定してくれます。
use axum::{routing::get, Json, Router};use serde::Serialize;
#[derive(Serialize)]struct Todo { id: u32, title: String, done: bool,}
#[tokio::main]async fn main() { let app = Router::new() .route("/", get(hello)) .route("/example", get(example_todo));
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap(); println!("Listening on http://127.0.0.1:3000"); axum::serve(listener, app).await.unwrap();}
async fn hello() -> &'static str { "こんにちは、世界!"}
async fn example_todo() -> Json<Todo> { let todo = Todo { id: 1, title: String::from("axumを学ぶ"), done: false, }; Json(todo)}-i フラグを付けると、curlはステータス行とヘッダーも表示します。
curl -i http://127.0.0.1:3000/exampleHTTP/1.1 200 OKcontent-type: application/jsoncontent-length: 45
{"id":1,"title":"axumを学ぶ","done":false}(curlは date ヘッダーも表示しますが、このページでは省いています。)content-length はバイト数です。日本語の文字はUTF-8で1文字3バイトなので、文字数より大きくなります。JSONのキーは、構造体のフィールド名そのままです。このJSONの形は、サーバーなしでPlaygroundで確かめられます。ただのserdeだからです。ここには、ステップ4で受け取るデータに使う NewTodo 構造体も入れてあります。
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Serialize)]struct Todo { id: u32, title: String, done: bool,}
#[derive(Debug, Deserialize)]struct NewTodo { title: String,}
fn main() { let todo = Todo { id: 1, title: String::from("axumを学ぶ"), done: false }; println!("{}", serde_json::to_string(&todo).unwrap());
let list = vec![todo.clone(), Todo { id: 2, title: String::from("牛乳を買う"), done: true }]; println!("{}", serde_json::to_string(&list).unwrap());
let input: NewTodo = serde_json::from_str(r#"{"title":"犬の散歩"}"#).unwrap(); println!("{:?}", input);
let bad = serde_json::from_str::<NewTodo>(r#"{"name":"犬の散歩"}"#); println!("{}", bad.unwrap_err());}実行結果:
{"id":1,"title":"axumを学ぶ","done":false}[{"id":1,"title":"axumを学ぶ","done":false},{"id":2,"title":"牛乳を買う","done":true}]NewTodo { title: "犬の散歩" }missing field `title` at line 1 column 23最後の行はserde_jsonのエラーメッセージで、「1行目の23列目で title フィールドが見つからない」という意味です。列の数はバイト単位で数えられます。Vec<Todo> はJSONの配列になります。これがまさに、GET /todos が返すものです。
ステップ3: パスパラメータ
Section titled “ステップ3: パスパラメータ”/todos/2 のようなパスでは、2 の部分がリクエストごとに変わります。axum 0.8では、ルートのその部分を {id} のように波かっこで示し、ハンドラの中で Path エクストラクタ(extractor)を使って読み取ります。
use axum::{extract::Path, routing::get, Router};
#[tokio::main]async fn main() { let app = Router::new() .route("/hello/{name}", get(hello_name)) .route("/add/{a}/{b}", get(add));
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap(); println!("Listening on http://127.0.0.1:3000"); axum::serve(listener, app).await.unwrap();}
async fn hello_name(Path(name): Path<String>) -> String { format!("こんにちは、{name}さん!")}
async fn add(Path((a, b)): Path<(i32, i32)>) -> String { format!("{a} + {b} = {}", a + b)}エクストラクタは、リクエストから1つの部分を取り出すハンドラの引数です。Path(name): Path<String> はパターンです。Path をほどいて、中の String を取り出します。パラメータが2つならタプルで受け取ります。
curl http://127.0.0.1:3000/hello/Anacurl http://127.0.0.1:3000/add/2/3こんにちは、Anaさん!2 + 3 = 5ここでうれしい驚きがあります。a と b は i32 だと書いたので、axumがそれをチェックしてくれるのです。数字でないものを送ってみましょう。
curl -i http://127.0.0.1:3000/add/2/xHTTP/1.1 400 Bad Requestcontent-type: text/plain; charset=utf-8content-length: 68
Invalid URL: Cannot parse value at index 1 with value `x` to a `i32`「URLが不正です。インデックス1の値 x を i32 にパースできません」という意味です。ハンドラは実行すらされていません。関数のシグネチャに書いた型が、そのまま入力チェックになっているのです。
ステップ4: POSTでJSONを受け取る
Section titled “ステップ4: POSTでJSONを受け取る”ToDoを作るとき、クライアントはリクエストのボディ(body)でJSONを送ります。Json エクストラクタはボディを読み、構造体にデシリアライズします。クライアントが送るのはタイトルだけなので、入力用には別の NewTodo 構造体を使います。
use axum::{routing::post, Json, Router};use serde::{Deserialize, Serialize};
#[derive(Serialize)]struct Todo { id: u32, title: String, done: bool,}
#[derive(Deserialize)]struct NewTodo { title: String,}
#[tokio::main]async fn main() { let app = Router::new().route("/echo", post(echo_todo));
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap(); println!("Listening on http://127.0.0.1:3000"); axum::serve(listener, app).await.unwrap();}
async fn echo_todo(Json(input): Json<NewTodo>) -> Json<Todo> { let todo = Todo { id: 1, title: input.title, done: false, }; Json(todo)}ルートでは get(...) の代わりに post(...) を使います。curlでは、-X POST でメソッドを指定し、-H でヘッダーを追加し、-d でボディを送ります。
curl -X POST http://127.0.0.1:3000/echo -H "Content-Type: application/json" -d '{"title":"牛乳を買う"}'{"id":1,"title":"牛乳を買う","done":false}ここでも、axumはコードが動く前に入力をチェックしてくれます。JSONに title がなければ、422 Unprocessable Entity が返ります。
curl -i -X POST http://127.0.0.1:3000/echo -H "Content-Type: application/json" -d '{"name":"牛乳を買う"}'HTTP/1.1 422 Unprocessable Entitycontent-type: text/plain; charset=utf-8content-length: 99
Failed to deserialize the JSON body into the target type: missing field `title` at line 1 column 26「JSONのボディを目的の型にデシリアライズできませんでした。title フィールドがありません」という意味です。
そして Content-Type ヘッダーを忘れると、Expected request with `Content-Type: application/json` (Content-Type: application/json のリクエストが必要です)というメッセージとともに 415 Unsupported Media Type が返ります。curlでテストするときに、いちばん忘れやすいのがこのヘッダーです。
ステップ5: 本物のToDo APIのための共有状態
Section titled “ステップ5: 本物のToDo APIのための共有状態”ここまでは、それぞれのハンドラがその場でデータを作っていました。本物のAPIは、リクエストをまたいでToDoを覚えておく必要があります。リクエストは同時に届くことがあり、別々のスレッドで処理されるので、一覧は安全に共有できなければなりません。それが、並行処理のページで出会ったコンビの役目です。
Mutex<Vec<Todo>>は、一度に1つのリクエストだけが一覧を変更できるようにします。Arc<...>は、たくさんのハンドラが同じ一覧へのポインタを所有できるようにします(スマートポインタを参照)。
type Db = Arc<Mutex<Vec<Todo>>>; で型に短い名前を付け、.with_state(db) でルーターに渡し、ハンドラでは State エクストラクタで読み取ります。axumはリクエストごとに Arc をクローンします(ポインタをコピーするだけなので軽い処理です)。
完成したプログラムは次のとおりです。src/main.rs をこの内容に置き換えます。
use std::sync::{Arc, Mutex};
use axum::{ extract::{Path, State}, http::StatusCode, routing::get, Json, Router,};use serde::{Deserialize, Serialize};
#[derive(Clone, Serialize)]struct Todo { id: u32, title: String, done: bool,}
#[derive(Deserialize)]struct NewTodo { title: String,}
// 「データベース」:すべてのリクエストで共有するToDoの一覧type Db = Arc<Mutex<Vec<Todo>>>;
#[tokio::main]async fn main() { let db: Db = Arc::new(Mutex::new(Vec::new()));
let app = Router::new() .route("/todos", get(list_todos).post(create_todo)) .route("/todos/{id}", get(get_todo).delete(delete_todo)) .with_state(db);
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap(); println!("Listening on http://127.0.0.1:3000"); axum::serve(listener, app).await.unwrap();}
// GET /todosasync fn list_todos(State(db): State<Db>) -> Json<Vec<Todo>> { let todos = db.lock().unwrap(); Json(todos.clone())}
// GET /todos/{id}async fn get_todo(State(db): State<Db>, Path(id): Path<u32>) -> Result<Json<Todo>, StatusCode> { let todos = db.lock().unwrap(); match todos.iter().find(|todo| todo.id == id) { Some(todo) => Ok(Json(todo.clone())), None => Err(StatusCode::NOT_FOUND), }}
// POST /todosasync fn create_todo(State(db): State<Db>, Json(input): Json<NewTodo>) -> (StatusCode, Json<Todo>) { let mut todos = db.lock().unwrap(); let id = todos.iter().map(|todo| todo.id).max().unwrap_or(0) + 1; let todo = Todo { id, title: input.title, done: false, }; todos.push(todo.clone()); (StatusCode::CREATED, Json(todo))}
// DELETE /todos/{id}async fn delete_todo(State(db): State<Db>, Path(id): Path<u32>) -> StatusCode { let mut todos = db.lock().unwrap(); let before = todos.len(); todos.retain(|todo| todo.id != id); if todos.len() < before { StatusCode::NO_CONTENT } else { StatusCode::NOT_FOUND }}新しく出てきた考え方を順に見ていきましょう。
- 1つのパスに複数のメソッド。
get(list_todos).post(create_todo)は、GET /todosとPOST /todosを別々のハンドラに振り分けます。 - ステータスコードは戻り値。
StatusCodeはaxumのhttpモジュールにあります。ハンドラはResultを返せます。Ok(Json(todo))はJSONつきの200に、Err(StatusCode::NOT_FOUND)は空の404になります。タプル(StatusCode::CREATED, Json(todo))はステータスとボディを両方設定します。StatusCode::NO_CONTENT(204)だけを返すのは、「完了しました。返すものはありません」という意味です。 lock()とガード。db.lock().unwrap()は、ほかのリクエストが一覧を使い終わるまで待ってから、ふつうのVecのように読み書きできるガードを渡してくれます。ロックは、関数の終わりでガードがスコープを抜けたときに解放されます。- 新しいid。次のidは、一覧の中でいちばん大きいidに1を足したものです。一覧が空なら1になります。
- 削除。
retainは、idが一致しないToDoをすべて残します。長さが変わらなければ何も削除されなかったということなので、404を返します。
なぜ非同期用のMutexではなく std::sync::Mutex を使うのでしょうか? .await をまたいでロックを持ち続けない限り、これで問題なく(しかも速く)動くからです。今回のハンドラはどれもそうしていません。ロックして、一覧を操作して、返すだけです。
一覧とロックのロジックにはWebサーバーが要らないので、Playgroundで試せます。次の関数は、axumを使わずにハンドラと同じ処理をします。
use std::sync::{Arc, Mutex};
#[derive(Clone, Debug)]struct Todo { id: u32, title: String, done: bool,}
type Db = Arc<Mutex<Vec<Todo>>>;
fn create(db: &Db, title: &str) -> Todo { let mut todos = db.lock().unwrap(); let id = todos.iter().map(|todo| todo.id).max().unwrap_or(0) + 1; let todo = Todo { id, title: title.to_string(), done: false }; todos.push(todo.clone()); todo}
fn get(db: &Db, id: u32) -> Option<Todo> { let todos = db.lock().unwrap(); todos.iter().find(|todo| todo.id == id).cloned()}
fn delete(db: &Db, id: u32) -> bool { let mut todos = db.lock().unwrap(); let before = todos.len(); todos.retain(|todo| todo.id != id); todos.len() < before}
fn main() { let db: Db = Arc::new(Mutex::new(Vec::new())); let second_handle = Arc::clone(&db); // axumがリクエストごとにしていること
println!("{:?}", create(&db, "牛乳を買う")); println!("{:?}", create(&second_handle, "axumを学ぶ")); println!("{:?}", get(&db, 2)); println!("{:?}", get(&db, 99)); println!("1を削除した? {}", delete(&db, 1)); println!("もう一度1を削除した? {}", delete(&db, 1)); println!("残り: {}", db.lock().unwrap().len());}実行結果:
Todo { id: 1, title: "牛乳を買う", done: false }Todo { id: 2, title: "axumを学ぶ", done: false }Some(Todo { id: 2, title: "axumを学ぶ", done: false })None1を削除した? trueもう一度1を削除した? false残り: 1second_handle を通して作ったToDoが、db から読んだときにも見えています。2つの Arc が同じ一覧を指しているからです。
ステップ6: curlでテストする
Section titled “ステップ6: curlでテストする”cargo run でサーバーを起動し、2つ目のターミナルですべてのエンドポイントを試してみましょう。最初、一覧は空です。
curl http://127.0.0.1:3000/todos[]ToDoを作ります。ステータスが 201 Created になっていることに注目してください。
curl -i -X POST http://127.0.0.1:3000/todos -H "Content-Type: application/json" -d '{"title":"牛乳を買う"}'HTTP/1.1 201 Createdcontent-type: application/jsoncontent-length: 47
{"id":1,"title":"牛乳を買う","done":false}2つ目を作って、すべてを一覧にします。
curl -X POST http://127.0.0.1:3000/todos -H "Content-Type: application/json" -d '{"title":"axumを学ぶ"}'curl http://127.0.0.1:3000/todos{"id":2,"title":"axumを学ぶ","done":false}[{"id":1,"title":"牛乳を買う","done":false},{"id":2,"title":"axumを学ぶ","done":false}]idで1つ取得し、存在しないものも要求してみます。
curl http://127.0.0.1:3000/todos/2curl -i http://127.0.0.1:3000/todos/99{"id":2,"title":"axumを学ぶ","done":false}HTTP/1.1 404 Not Foundcontent-length: 0ToDo 1を削除し、もう一度削除してみます。
curl -i -X DELETE http://127.0.0.1:3000/todos/1curl -i -X DELETE http://127.0.0.1:3000/todos/1HTTP/1.1 204 No Content
HTTP/1.1 404 Not Foundcontent-length: 0curl http://127.0.0.1:3000/todos[{"id":2,"title":"axumを学ぶ","done":false}]axumは、いくつかの間違いにも対応してくれます。JSONの型が違えば問題を説明する422が返り、そのパスに設定していないメソッドを使うと 405 Method Not Allowed が返ります。このとき、使えるメソッドの一覧がヘッダーに入っています。
curl -i -X POST http://127.0.0.1:3000/todos -H "Content-Type: application/json" -d '{"title": 42}'curl -i -X PUT http://127.0.0.1:3000/todos/2HTTP/1.1 422 Unprocessable Entitycontent-type: text/plain; charset=utf-8content-length: 130
Failed to deserialize the JSON body into the target type: title: invalid type: integer `42`, expected a string at line 1 column 12HTTP/1.1 405 Method Not Allowedallow: GET,HEAD,DELETEcontent-length: 0422のメッセージは「title の型が不正です。整数 42 ではなく文字列が必要です」という意味です。
Ctrl + C でサーバーを止めて、もう一度起動してみてください。一覧は空になっています。データはメモリの中にしかないからです。データを保存する方法は、下のアイデアの最初に紹介します。
よくある間違い
Section titled “よくある間違い”古い :id のルート構文を使う。ネット上のチュートリアルの多くは、/todos/:id という書き方を使っていたaxum 0.7向けに書かれています。axum 0.8でこう書くとコンパイルは通りますが、プログラムは起動したとたんにパニックします。
thread 'main' (12345) panicked at src/main.rs:32:10:Path segments must not start with `:`. For capture groups, use `{capture}`. If you meant to literally match a segment starting with a colon, call `without_v07_checks` on the router.「パスのセグメントを : で始めてはいけません。キャプチャには {capture} を使ってください」という意味です。直し方は、/todos/{id} と書くことです。
Json をほかのエクストラクタより前に置く。リクエストのボディは1回しか読めないので、ボディを読むエクストラクタ(Json)は最後の引数にしなければなりません。create_todo(Json(input): Json<NewTodo>, State(db): State<Db>) と書いても、エラーはそのことを直接は教えてくれません。大事なのは次の行です。
error[E0277]: the trait bound `fn(Json<NewTodo>, State<...>) -> ... {create_todo}: Handler<_, ...>` is not satisfied |31 | .route("/todos", get(list_todos).post(create_todo)) | ---- ^^^^^^^^^^^ unsatisfied trait bound | = note: Consider using `#[axum::debug_handler]` to improve the error message「Handler is not satisfied」(Handler トレイトを満たしていない)と表示されたら、引数の順番を確認しましょう。また、すべての戻り値の型が、axumがレスポンスに変換できるものになっているかも確認してください。
curlで -H "Content-Type: application/json" を忘れる。サーバーは415を返します。Json エクストラクタには、このヘッダーが必要です。
再起動を忘れる。動いているサーバーは古いプログラムのままです。変更するたびに Ctrl + C で止めて、もう一度 cargo run しましょう。
拡張のアイデア
Section titled “拡張のアイデア”基本のAPIが動いたら、次のステップに挑戦してみましょう。1つずつ試してください。
- ToDoを完了にする。2つ目のルートに
.patch(...)でPATCH /todos/{id}を追加します。iter_mut()でToDoを見つけて、done = trueにして、それを返します。 - 入力チェック。空のタイトルは許可するべきではありません。ハンドラは
Result<(StatusCode, Json<Todo>), (StatusCode, String)>を返せるので、Err((StatusCode::BAD_REQUEST, String::from("タイトルを空にすることはできません")))を送り返します。チェックにはtitle.trim().is_empty()を使いましょう。 - 重複しないid。「いちばん大きいidに1を足す」方法だと、いちばん新しいToDoを削除したときに、そのidがまた使われてしまいます。代わりに、状態の中に別のカウンターを持ちましょう。たとえば
Mutexの中に、todos: Vec<Todo>とnext_id: u32を持つ構造体を入れます。 - JSONファイルへの保存。
TodoにもDeserializeをderiveします。起動時にstd::fs::read_to_stringとserde_json::from_strでtodos.jsonを読み込みます(ファイルがなければ空から始めます)。変更するたびに、serde_json::to_string_prettyとstd::fs::writeで一覧を書き戻します。必要なことはファイルの入出力とRustでJSONを扱うのページにすべて載っています。あとで、ファイルをSQLiteのような本物のデータベースに置き換えることもできます。 - tower-httpでログを出す。
cargo add tower-http --features traceとcargo add tracing-subscriberを実行して、ログ用のレイヤーを追加します。するとサーバーは、リクエストごとに、メソッド、パス、ステータス、かかった時間を1行表示するようになります(ログについてはログ出力を参照)。
use tower_http::trace::TraceLayer;
// main の最初で:tracing_subscriber::fmt() .with_max_level(tracing_subscriber::filter::LevelFilter::DEBUG) .init();
// ルーターを作るときに:let app = Router::new() .route("/todos", get(list_todos).post(create_todo)) .route("/todos/{id}", get(get_todo).delete(delete_todo)) .layer(TraceLayer::new_for_http()) .with_state(db);- デプロイについてのメモ。
127.0.0.1は、自分のコンピュータからのリクエストしか受け付けません。ほかのマシンからのリクエストを受け付けるには(たとえばDockerの中やサーバー上で動かすときは)、代わりに0.0.0.0にバインドします。多くのホスティングサービスは、使うポートをPORT環境変数でプログラムに伝えます。これはstd::env::varで読み取れます(環境変数とプロセスを参照)。cargo build --releaseでビルドすると、サーバーはずっと速くなります。できあがるのはtarget/release/の中のファイル1つだけで、それをLinuxサーバーにコピーしたり、小さなDockerイメージに入れたりできます。公開するサイトなら、HTTPSを担当するリバースプロキシ(Caddyやnginxなど)の後ろに置きましょう。
- ハンドラは
async fnです。ルーターはメソッドとパスをハンドラに結びつけます。axum::serveは、それをtokioのTcpListenerの上で動かします。 - エクストラクタはリクエストからデータを取り出します。URLの一部(
/todos/{id})にはPath、ボディにはJson(必ず最後)、共有データにはStateを使います。 - JSONには
Json(value)を、ステータスにはStatusCodeを返します。両方を設定するならタプル、成功とエラーのステータスを切り替えるならResultを返します。 Arc<Mutex<Vec<Todo>>>で、1つの一覧をすべてのリクエストで安全に共有できます。ロックして、使って、ガードを破棄させましょう。.awaitをまたいでロックを持ち続けてはいけません。- APIは
curlでテストします。-iでステータスを表示し、-Xでメソッドを指定し、-Hでヘッダーを追加し、-dでボディを送ります。
理解度チェック
Section titled “理解度チェック”理解度チェック
1. axum 0.8で、パスにidを含むルートはどう書きますか?
2. ハンドラが (StatusCode::CREATED, Json(todo)) を返します。クライアントは何を受け取りますか?
3. 共有する一覧を Arc<Mutex<...>> で包むのはなぜですか?
4. ハンドラの引数で、最後に置かなければならないエクストラクタはどれですか?
5. curl -X POST でJSONを送りましたが、Content-Type: application/json ヘッダーを忘れました。axumは何を返しますか?
スコア: 0 / 5
さらに詳しく
Section titled “さらに詳しく”- axum のドキュメント — docs.rs(英語):ルーティング、ハンドラ、レスポンスの節がある公式ガイドです。
- axum のエクストラクタ — docs.rs(英語):組み込みのエクストラクタのすべてと、その順番のルールが載っています。
- Tokio チュートリアル(英語):axumを動かしている非同期ランタイムを、状態の共有も含めて学べます。
- tower-http のドキュメント — docs.rs(英語):ログ、CORS、圧縮、タイムアウトのための、すぐに使えるレイヤーがそろっています。
- HTTP レスポンスステータスコード — MDN:200、201、204、404、422などが何を意味するのかがわかります。