Rust で axum を使って REST API を作ってみる
このスクラップについて
山とコードさんの記事に影響を受けたので axum で REST API を作る過程を記録する。
フレームワーク比較
この辺りが人気のようだ。
やはり axum が一番勢いがあるのかな
公式ドキュメント
どうやらこの README ページのようなものが公式ドキュメントらしい。
Actic Web などは公式サイトがある、こういうのがあるとちょっと信頼感がアップするから不思議だ。
プロジェクト作成
cargo new first_rest_api
依存関係の追加
[package]
name = "first_rest_api"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
axum = "0.7.5"
tokio の追加
どうやら tokio というものが必要なようだ。
cargo add tokio --features macros,rt-multi-thread
コーディング
use axum::{routing::get, Router};
#[tokio::main]
async fn main() {
let app = Router::new().route("/", get(|| async { "Hello, World!" }));
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
実行
cargo run
curl http://localhost:3000
Hello, World!
ルーティング
use axum::{routing::get, Router};
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/", get(root))
.route("/foo", get(get_foo).post(post_foo))
.route("/foo/bar", get(foo_bar));
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
async fn root() -> String {
String::from("root\n")
}
async fn get_foo() -> String {
String::from("get_foo\n")
}
async fn post_foo() -> String {
String::from("post_foo\n")
}
async fn foo_bar() -> String {
String::from("foo_bar\n")
}
curl http://localhost:3000/
curl http://localhost:3000/foo
curl -X POST http://localhost:3000/foo
curl http://localhost:3000/foo/bar
curl -v http://localhost:3000/not-found
root
get_foo
post_foo
foo_bar
* Trying 127.0.0.1:3000...
* Connected to localhost (127.0.0.1) port 3000 (#0)
> GET /not-found HTTP/1.1
> Host: localhost:3000
> User-Agent: curl/8.1.2
> Accept: */*
>
< HTTP/1.1 404 Not Found
< content-length: 0
< date: Thu, 30 May 2024 06:59:28 GMT
<
* Connection #0 to host localhost left intact
自動再起動
cargo-watch を使えば良いようだ。
まずは下記でインストールする。
cargo install cargo-watch --locked
cargo watch -x run
これで変更が反映されるようになった。
次は Responses
JSON 出力を試してみたい。
JSON 出力
serde_json が必要なようだ。
cargo add serde_json
use axum::{routing::get, Json, Router};
use serde_json::{json, Value};
#[tokio::main]
async fn main() {
let app = Router::new().route("/json", get(json));
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
async fn json() -> Json<Value> {
Json(json!({ "data": 42 }))
}
use axum::{routing::get, Json, Router};
use serde_json::{json, Value};
#[tokio::main]
async fn main() {
let app = Router::new().route("/json", get(json));
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
async fn json() -> Json<Value> {
Json(json!({ "data": 42 }))
}
cargo watch -x run
curl http://localhost:3000/json
{"data":42}
下層ページを見つけた
この他にも docs ディレクトリに色々な情報がありそうだ。
エラー処理
axum ではシンプルにエラーをレスポンスに変換でき、また、すべてのエラーが処理されていることを保証できるようだ。
Infallible という列挙型が出てくるようだ、決して起こらないエラーのための型らしいが何に使うんだろう。
ドキュメントによると必ず Ok な Result 型を表現するのに使うようだ。
anyhow とはエラー処理のクレートのようだ。
Ok::<_, anyhow::Error>(Response::new(Body::empty()))
という書き方はクロージャなどが関わっているのだろうか?
Rust の知識が足りなすぎて消化できない。。。
もう一度 Hello World に戻る
use axum::{routing::get, Router};
#[tokio::main]
async fn main() {
let app = Router::new().route("/", get(|| async { "Hello, World!" }));
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
cargo watch -x run
curl http://localhost:3000/
Hello, World!
Router を極める
1 つ 1 つの構成要素を深掘りしていこう。
route メソッド
pub fn route(self, path: &str, method_router: MethodRouter<S>) -> Self
MethodRouter というものがあるがこれは通常は get や put でラップしたハンドラーになるらしい。
ハンドラーについては別の機会に学んでいこう。
/foo
と /foo/
は別のパスになるのかな?→ 少し試した所そのようだ。
Captures
キャプチャーを使うと下記のようなパスにマッチさせることができる。
- /:key
- /users/:id
- /users/:id/tweets
キャプチャされた値は Path と呼ばれるエクストラクターを使うことで取得できるようだ。
MatchedPath を使うことで /:key
や /users/:id
のようなルート設定に使ったパスの文字列を取得できる。
セグメントが特定の数値や正規表現にマッチしているかどうかをチェックすることはできず、それについてはハンドラーで手動でチェックする必要があるようだ。
次は Wildcards