Rust で axum を使って REST API を作ってみる
![薄田達哉 / tatsuyasusukida](https://res.cloudinary.com/zenn/image/fetch/s--h04js5hX--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/02d3bdd44b.jpeg)
このスクラップについて
山とコードさんの記事に影響を受けたので axum で REST API を作る過程を記録する。
![薄田達哉 / tatsuyasusukida](https://res.cloudinary.com/zenn/image/fetch/s--h04js5hX--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/02d3bdd44b.jpeg)
フレームワーク比較
この辺りが人気のようだ。
やはり axum が一番勢いがあるのかな
![薄田達哉 / tatsuyasusukida](https://res.cloudinary.com/zenn/image/fetch/s--h04js5hX--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/02d3bdd44b.jpeg)
公式ドキュメント
どうやらこの README ページのようなものが公式ドキュメントらしい。
Actic Web などは公式サイトがある、こういうのがあるとちょっと信頼感がアップするから不思議だ。
![薄田達哉 / tatsuyasusukida](https://res.cloudinary.com/zenn/image/fetch/s--h04js5hX--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/02d3bdd44b.jpeg)
プロジェクト作成
cargo new first_rest_api
![薄田達哉 / tatsuyasusukida](https://res.cloudinary.com/zenn/image/fetch/s--h04js5hX--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/02d3bdd44b.jpeg)
依存関係の追加
[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"
![薄田達哉 / tatsuyasusukida](https://res.cloudinary.com/zenn/image/fetch/s--h04js5hX--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/02d3bdd44b.jpeg)
tokio の追加
どうやら tokio というものが必要なようだ。
cargo add tokio --features macros,rt-multi-thread
![薄田達哉 / tatsuyasusukida](https://res.cloudinary.com/zenn/image/fetch/s--h04js5hX--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/02d3bdd44b.jpeg)
コーディング
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();
}
![薄田達哉 / tatsuyasusukida](https://res.cloudinary.com/zenn/image/fetch/s--h04js5hX--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/02d3bdd44b.jpeg)
実行
cargo run
curl http://localhost:3000
Hello, World!
![薄田達哉 / tatsuyasusukida](https://res.cloudinary.com/zenn/image/fetch/s--h04js5hX--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/02d3bdd44b.jpeg)
ルーティング
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
![薄田達哉 / tatsuyasusukida](https://res.cloudinary.com/zenn/image/fetch/s--h04js5hX--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/02d3bdd44b.jpeg)
自動再起動
cargo-watch を使えば良いようだ。
まずは下記でインストールする。
cargo install cargo-watch --locked
cargo watch -x run
これで変更が反映されるようになった。
![薄田達哉 / tatsuyasusukida](https://res.cloudinary.com/zenn/image/fetch/s--h04js5hX--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/02d3bdd44b.jpeg)
次は Responses
JSON 出力を試してみたい。
![薄田達哉 / tatsuyasusukida](https://res.cloudinary.com/zenn/image/fetch/s--h04js5hX--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/02d3bdd44b.jpeg)
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}
![薄田達哉 / tatsuyasusukida](https://res.cloudinary.com/zenn/image/fetch/s--h04js5hX--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/02d3bdd44b.jpeg)
下層ページを見つけた
この他にも docs ディレクトリに色々な情報がありそうだ。
![薄田達哉 / tatsuyasusukida](https://res.cloudinary.com/zenn/image/fetch/s--h04js5hX--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/02d3bdd44b.jpeg)
エラー処理
axum ではシンプルにエラーをレスポンスに変換でき、また、すべてのエラーが処理されていることを保証できるようだ。
Infallible という列挙型が出てくるようだ、決して起こらないエラーのための型らしいが何に使うんだろう。
ドキュメントによると必ず Ok な Result 型を表現するのに使うようだ。
anyhow とはエラー処理のクレートのようだ。
Ok::<_, anyhow::Error>(Response::new(Body::empty()))
という書き方はクロージャなどが関わっているのだろうか?
Rust の知識が足りなすぎて消化できない。。。
![薄田達哉 / tatsuyasusukida](https://res.cloudinary.com/zenn/image/fetch/s--h04js5hX--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/02d3bdd44b.jpeg)
もう一度 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!
![薄田達哉 / tatsuyasusukida](https://res.cloudinary.com/zenn/image/fetch/s--h04js5hX--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/02d3bdd44b.jpeg)
Router を極める
1 つ 1 つの構成要素を深掘りしていこう。
![薄田達哉 / tatsuyasusukida](https://res.cloudinary.com/zenn/image/fetch/s--h04js5hX--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/02d3bdd44b.jpeg)
route メソッド
pub fn route(self, path: &str, method_router: MethodRouter<S>) -> Self
MethodRouter というものがあるがこれは通常は get や put でラップしたハンドラーになるらしい。
ハンドラーについては別の機会に学んでいこう。
/foo
と /foo/
は別のパスになるのかな?→ 少し試した所そのようだ。
![薄田達哉 / tatsuyasusukida](https://res.cloudinary.com/zenn/image/fetch/s--h04js5hX--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/02d3bdd44b.jpeg)
Captures
キャプチャーを使うと下記のようなパスにマッチさせることができる。
- /:key
- /users/:id
- /users/:id/tweets
キャプチャされた値は Path と呼ばれるエクストラクターを使うことで取得できるようだ。
MatchedPath を使うことで /:key
や /users/:id
のようなルート設定に使ったパスの文字列を取得できる。
セグメントが特定の数値や正規表現にマッチしているかどうかをチェックすることはできず、それについてはハンドラーで手動でチェックする必要があるようだ。
![薄田達哉 / tatsuyasusukida](https://res.cloudinary.com/zenn/image/fetch/s--h04js5hX--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/02d3bdd44b.jpeg)
次は Wildcards