コンテンツにスキップ

RustとAxumで作るWeb API

このプロジェクトでは、本物のWebサーバーを作ります。小さな ToDo API です。ほかのプログラム(Webサイト、スマホアプリ、あるいはターミナルの curl)がHTTPリクエストを送ると、サーバーはJSONで答えます。ふだん使っているアプリの多くで「バックエンド」を動かしているのも、これと同じしくみです。

使うのは axum です。Tokioチームが作った、人気があって使いやすいWebフレームワークです。axumは非同期(async)のRustと serde の上に作られているので、このプロジェクトではこれまでに学んだことをたくさん組み合わせます。構造体、Result、スマートポインタ、スレッド間での状態の共有などです。ToDoリストCLIを作ったことがあれば、考え方には見覚えがあるはずです。今回は、ToDoリストがWebのアドレスの向こうにあります。

完成すると、サーバーは次のリクエストに答えられるようになります。

メソッドとパス すること 成功したときのステータス
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」ボタンを用意しています。

新しいプロジェクトを作り、4つのクレートを追加します。

Terminal window
cargo new todo-api
cd todo-api
cargo add [email protected] tokio --features tokio/full
cargo add serde --features derive
cargo 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 と書くと、別のコマンドになってしまいます)。

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つターミナルを開き、リクエストを送ってみましょう。

Terminal window
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はステータス行とヘッダーも表示します。

Terminal window
curl -i http://127.0.0.1:3000/example
HTTP/1.1 200 OK
content-type: application/json
content-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 が返すものです。

/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つならタプルで受け取ります。

Terminal window
curl http://127.0.0.1:3000/hello/Ana
curl http://127.0.0.1:3000/add/2/3
こんにちは、Anaさん!
2 + 3 = 5

ここでうれしい驚きがあります。a と b は i32 だと書いたので、axumがそれをチェックしてくれるのです。数字でないものを送ってみましょう。

Terminal window
curl -i http://127.0.0.1:3000/add/2/x
HTTP/1.1 400 Bad Request
content-type: text/plain; charset=utf-8
content-length: 68
Invalid URL: Cannot parse value at index 1 with value `x` to a `i32`

「URLが不正です。インデックス1の値 x を i32 にパースできません」という意味です。ハンドラは実行すらされていません。関数のシグネチャに書いた型が、そのまま入力チェックになっているのです。

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 でボディを送ります。

Terminal window
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 が返ります。

Terminal window
curl -i -X POST http://127.0.0.1:3000/echo -H "Content-Type: application/json" -d '{"name":"牛乳を買う"}'
HTTP/1.1 422 Unprocessable Entity
content-type: text/plain; charset=utf-8
content-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 /todos
async 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 /todos
async 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 })
None
1を削除した? true
もう一度1を削除した? false
残り: 1
▶ 実際に試してみよう

second_handle を通して作ったToDoが、db から読んだときにも見えています。2つの Arc が同じ一覧を指しているからです。

cargo run でサーバーを起動し、2つ目のターミナルですべてのエンドポイントを試してみましょう。最初、一覧は空です。

Terminal window
curl http://127.0.0.1:3000/todos
[]

ToDoを作ります。ステータスが 201 Created になっていることに注目してください。

Terminal window
curl -i -X POST http://127.0.0.1:3000/todos -H "Content-Type: application/json" -d '{"title":"牛乳を買う"}'
HTTP/1.1 201 Created
content-type: application/json
content-length: 47
{"id":1,"title":"牛乳を買う","done":false}

2つ目を作って、すべてを一覧にします。

Terminal window
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つ取得し、存在しないものも要求してみます。

Terminal window
curl http://127.0.0.1:3000/todos/2
curl -i http://127.0.0.1:3000/todos/99
{"id":2,"title":"axumを学ぶ","done":false}
HTTP/1.1 404 Not Found
content-length: 0

ToDo 1を削除し、もう一度削除してみます。

Terminal window
curl -i -X DELETE http://127.0.0.1:3000/todos/1
curl -i -X DELETE http://127.0.0.1:3000/todos/1
HTTP/1.1 204 No Content
HTTP/1.1 404 Not Found
content-length: 0
Terminal window
curl http://127.0.0.1:3000/todos
[{"id":2,"title":"axumを学ぶ","done":false}]

axumは、いくつかの間違いにも対応してくれます。JSONの型が違えば問題を説明する422が返り、そのパスに設定していないメソッドを使うと 405 Method Not Allowed が返ります。このとき、使えるメソッドの一覧がヘッダーに入っています。

Terminal window
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/2
HTTP/1.1 422 Unprocessable Entity
content-type: text/plain; charset=utf-8
content-length: 130
Failed to deserialize the JSON body into the target type: title: invalid type: integer `42`, expected a string at line 1 column 12
HTTP/1.1 405 Method Not Allowed
allow: GET,HEAD,DELETE
content-length: 0

422のメッセージは「title の型が不正です。整数 42 ではなく文字列が必要です」という意味です。

Ctrl + C でサーバーを止めて、もう一度起動してみてください。一覧は空になっています。データはメモリの中にしかないからです。データを保存する方法は、下のアイデアの最初に紹介します。

古い :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 しましょう。

基本の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 でボディを送ります。

理解度チェック

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