Rust(axum, sqlx)とReactでWebアプリ開発 - 備忘録

## はじめに
### この記事を書いている人
Rust初学者。
Udemyと[the book](https://doc.rust-jp.rs/book-ja/)を軽く一周したぐらい。the bookは後半さらっと流しているので正直あまり理解はできてない。Rust難しい。けどおもしろい。

近年、フロントエンド業界でもRustがじわじわ注目されているということでRustに興味を持った。
せっかくなら何か作ってみようということで取り組んでみた。

:::note warn
本記事は筆者の備忘録的な側面が多いため、読みにくい部分があります。
また、必ずしも正しい情報ではなかったり、実装における最適解ではない可能性があります。
:::
※もしより正確な情報だったり詳細な内容をご存じの方はコメントで教えていただけると幸いです。

## 背景
https://www.shuwasystem.co.jp/book/9784798067315.html

こちらの書籍を参考に実装を進めているが、2024年7月現在では執筆当時と各クレートのバージョンが異なる。

- axum:
    - 書籍:0.4.8 → 0.7.5
https://docs.rs/axum/0.7.5/axum/index.html
- hyper
    - 書籍:0.14.16 → 1.4.1
https://docs.rs/hyper/latest/hyper/

せっかくなら最新バージョンの書き方で実装しようとしたが、やはり苦労したので備忘録として残す。

※書籍から引用したソースコードはファイル名の後ろに(書籍版)と記す。
　()の記載がないものは、修正後のソースコードとする。
 ※後半はRustクレートのバージョン関連ではなく、純粋にクオリティUPのコード修正が多い

## 第3章

### Cargo.toml
3章は以下の内容で進める。
おそらく最新なはず。

```Cargo.toml
[package]
name = "todo"
version = "0.1.0"
edition = "2021"

[dependencies]
anyhow = "1.0.86"
axum = "0.7.5"
hyper = { version = "1.4.1", features = ["full"] }
mime = "0.3.17"
serde = { version = "1.0.204", features = ["derive"] }
serde_json = "1.0.120"
thiserror = "1.0.63"
tokio = { version = "1", features = ["full"] }
tower = "0.4.13"
tracing = "0.1.40"
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
```

### 3.2 Hello, world
いきなりつまづく。
`axum::Server`が使えなくなっていた。


https://qiita.com/raiga0310/items/da2049912eae68f1a0c2

axum v0.7以降ではこのように書く。（公式ドキュメントのExampleも同じように書いてある）

```rust:main.rs
#[tokio::main]
async fn main() {

/* 省略 */

    // axum 0.4.8
    // let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
    // axum::Server::bind(&addr).serve(app.into_make_service()).await.unwrap();

    // axum 0.7.5
    let listener = tokio::net::TcpListener::bind("127.0.0.1:3000")
        .await
        .unwrap();
    tracing::debug!("listening on {:?}", listener);
    axum::serve(listener, app).await.unwrap();
}
```

### 3.3 テスト

`hyper::body::to_bytes`がhyper v1.0を境になくなっている？ので使えなさそうだった。

https://zenn.dev/naughie/articles/e130d0304cad2e

https://panicbit.github.io/fcm-rust/hyper/body/fn.to_bytes.html

「`hyper::Body`がなくなり、`axum::body::Body`に移行した」という情報を頼りにドキュメントを漁っていたところ、たまたま以下を発見した。

https://docs.rs/axum/latest/axum/body/fn.to_bytes.html

引数に`limit: usize`が追加されている。

```rust:main.rs
#[cfg(test)]
mod test {
    use std::usize;
    use super::*;
    use axum::{
        body::Body,
        body::to_bytes,
        http::{header, Method, Request},
    };
    use tower::ServiceExt;

    #[tokio::test]
    async fn should_return_hello_world() {
        let req = Request::builder().uri("/").body(Body::empty()).unwrap();
        let res = create_app().oneshot(req).await.unwrap();
        // axum 0.4.8, hyper 0.14.16
        // let bytes = hyper::body::to_bytes(res.into_body()).await.unwrap();
        // axum 0.7.5, hyper 1.4.1
        let bytes = to_bytes(res.into_body(), usize::MAX).await.unwrap();
        let body: String = String::from_utf8(bytes.to_vec()).unwrap();
        assert_eq!(body, "Hello, World!")
    }
}
```

### 3.4 Todo情報を保存する

`post`の呼び出しでコンパイルエラーになる。

```rust:main.rs(書籍版)
fn create_app<T: TodoRepository>(repository: T) -> Router {
    Router::new()
        .route("/", get(root))
        .route("/todos", post(create_todo::<T>))
        .layer(Extension(Arc::new(repository)))
}

pub async fn create_todo<T: TodoRepository>(
    Json(payload): Json<CreateTodo>,
    Extension(repository): Extension<Arc<T>>,
) -> impl IntoResponse {
    let todo = repository.create(payload);

    (StatusCode::CREATED, Json(todo))
}
```

```
error[E0277]: the trait bound `fn(Json<CreateTodo>, Extension<Arc<T>>) -> impl Future<Output = impl IntoResponse> {create_todo::<T>}: 
Handler<_, _>` is not satisfied
   --> src/main.rs:40:31
    |
40  |         .route("/todos", post(create_todo::<T>))
    |                          ---- ^^^^^^^^^^^^^^^^ the trait `Handler<_, _>` is not implemented for fn item `fn(Json<CreateTodo>, Extension<Arc<T>>) -> impl Future<Output = impl IntoResponse> {create_todo::<T>}`
    |                          |
    |                          required by a bound introduced by this call
    |
    = help: the following other types implement trait `Handler<T, S>`:
              <MethodRouter<S> as Handler<(), S>>
              <axum::handler::Layered<L, H, T, S> as Handler<T, S>>
note: required by a bound in `post`
   --> C:\Users\mrhd\.cargo\registry\src\index.crates.io-6f17d22bba15001f\axum-0.7.5\src\routing\method_routing.rs:389:1
    |
389 | top_level_handler_fn!(post, POST);
    | ^^^^^^^^^^^^^^^^^^^^^^----^^^^^^^
    | |                     |
    | |                     required by a bound in this function
    | required by this bound in `post`
    = note: this error originates in the macro `top_level_handler_fn` (in Nightly builds, run with -Z macro-backtrace for more info)  
```

以下の記事を参考にして、`create_todo`の引数の順番を入れ替えると解消した。

https://qiita.com/Sicut_study/items/5e5d6cce5ba48c225367



```rust:main.rs
pub async fn create_todo<T: TodoRepository>(
    Extension(repository): Extension<Arc<T>>,
    Json(payload): Json<CreateTodo>,
) -> impl IntoResponse {
    let todo = repository.create(payload);

    (StatusCode::CREATED, Json(todo))
}
```

### 3.5 httpリクエスト

```rust:handlers.rs(書籍版)
pub async fn update_todo<T: TodoRepository>(
    Path(id): Path<i32>,
    Json(payload): Json<UpdateTodo>,
    Extension(repository): Extension<Arc<T>>,
) -> Result<impl IntoResponse, StatusCode> {
    todo!();
    Ok(StatusCode::OK)
}
```

```rust:main.rs(書籍版)
fn create_app<T: TodoRepository>(repository: T) -> Router {
    Router::new()
        .route("/", get(root))
        .route("/todos", post(create_todo::<T>).get(all_todo::<T>))
        .route(
            "/todos/:id",
            get(find_todo::<T>)
                .delete(delete_todo::<T>)
                .patch(update_todo::<T>),
        )
        .layer(Extension(Arc::new(repository)))
}
```

`update_todo::<T>`の呼び出しでエラーになった。

```
error[E0277]: the trait bound `fn(axum::extract::Path<i32>, Json<UpdateTodo>, Extension<Arc<T>>) -> impl Future<Output = Result<impl IntoResponse, StatusCode>> {update_todo::<T>}: Handler<_, _>` is not satisfied
   --> src/main.rs:42:24
    |
42  |                 .patch(update_todo::<T>),
    |                  ----- ^^^^^^^^^^^^^^^^ the trait `Handler<_, _>` is not implemented for fn item `fn(Path<i32>, Json<UpdateTodo>, Extension<Arc<T>>) -> ... {update_todo::<...>}`
    |                  |
    |                  required by a bound introduced by this call
    |
    = help: the following other types implement trait `Handler<T, S>`:
              <MethodRouter<S> as Handler<(), S>>
              <axum::handler::Layered<L, H, T, S> as Handler<T, S>>
note: required by a bound in `MethodRouter::<S>::patch`
   --> /Users/01051530/.cargo/registry/src/index.crates.io-6f17d22bba15001f/axum-0.7.5/src/routing/method_routing.rs:589:5
    |
589 |     chained_handler_fn!(patch, PATCH);
    |     ^^^^^^^^^^^^^^^^^^^^-----^^^^^^^^
    |     |                   |
    |     |                   required by a bound in this associated function
    |     required by this bound in `MethodRouter::<S>::patch`
    = note: this error originates in the macro `chained_handler_fn` (in Nightly builds, run with -Z macro-backtrace for more info)
```

これも同じく`Extension`の引数の順番を変えるとエラーが解消した。

```rust:handlers.rs
pub async fn update_todo<T: TodoRepository>(
    Extension(repository): Extension<Arc<T>>,
    Path(id): Path<i32>,
    Json(payload): Json<UpdateTodo>,
) -> Result<impl IntoResponse, StatusCode> {
    todo!();
    Ok(StatusCode::OK)
}
```

### 3.6 バリデーションの追加
ここが苦労した末にエラーが解決できなかった。

```rust:handlers.rs(書籍版)
#[derive(Debug)]
pub struct ValidatedJson<T>(T);

#[async_trait]
impl<T, B> FromRequest<B> for ValidatedJson<T>
where
    T: DeserializeOwned + Validate,
    B: http_body::Body + Send,
    B::Data: Send,
    B::Error: Into<BoxError>,
{
    type Rejection = (StatusCode, String);

    async fn from_request(req: &mut RequestParts<B>) -> Result<Self, Self::Rejection> {
        let Json(value) = Json::<T>::from_request(req).await.map_err(|rejection| {
            let message = format!("Json parse error: [{}]", rejection);
            (StatusCode::BAD_REQUEST, message)
        })?;
        value.validate().map_err(|rejection| {
            let message = format!("Validation error: [{}]", rejection).replace('\n', ", ");
            (StatusCode::BAD_REQUEST, message)
        })?;
        Ok(ValidatedJson(value))
    }
}
```

`from_request`の`RequestParts<B>`という型がaxum0.7系では存在せず（axum0.6系を境になくなっている様子）、コンパイルエラーとなる。

chatGPTの力を借りて以下のような形までたどり着いたが、`Json::<T>::from_request`に渡す引数の型がよろしくないようだった。

```rust:handlers.rs(一部修正したがエラー)
#[derive(Debug)]
pub struct ValidatedJson<T>(T);

#[async_trait]
impl<T, S> FromRequestParts<S> for ValidatedJson<T>
where
    T: DeserializeOwned + Validate,
    S: Send + Sync,
{
    type Rejection = (StatusCode, String);

    async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
        let Json(value) = Json::<T>::from_request(parts, state).await.map_err(|rejection| {
            let message = format!("Json parse error: [{}]", rejection);
            (StatusCode::BAD_REQUEST, message)
        })?;
        value.validate().map_err(|rejection| {
            let message = format!("Validation error: [{}]", rejection).replace('\n', ", ");
            (StatusCode::BAD_REQUEST, message)
        })?;
        Ok(ValidatedJson(value))
    }
}
```
```
error[E0308]: mismatched types
  --> src\handlers.rs:87:51
   |
87 |         let Json(value) = Json::<T>::from_request(parts, state).await.map_err(|rejection| {
   |                           ----------------------- ^^^^^ expected `Request<Body>`, found `&mut Parts`
   |                           |
   |                           arguments to this function are incorrect
   |
   = note:         expected struct `axum::http::Request<Body>`
           found mutable reference `&'life0 mut axum::http::request::Parts`
note: associated function defined here
  --> C:\Users\mrhd\.cargo\registry\src\index.crates.io-6f17d22bba15001f\axum-core-0.4.3\src\extract\mod.rs:85:14
   |
85 |     async fn from_request(req: Request, state: &S) -> Result<Self, Self::Rejection>;
   |              ^^^^^^^^^^^^

For more information about this error, try `rustc --explain E0308`.
error: could not compile `todo` (bin "todo") due to 1 previous error
```

そこで、ハンドラ関数の引数にバリデーション後の値を渡すのではなく、
ハンドラ関数で値を受け取った後に自力でバリデーションをかけるようにした。

```rust:handlers.rs
use crate::repositories::{CreateTodo, TodoRepository, UpdateTodo};
use axum::{extract::Path, http::StatusCode, response::IntoResponse, Extension, Json};
use std::sync::Arc;

const ERR_STR_EMPTY: &str = "Error!: Can not be Empty";
const ERR_STR_OVER: &str = "Error!: Over text length";
const ERR_STR_NOT_FOUND: &str = "Todo not found";

pub async fn create_todo<T: TodoRepository>(
    Extension(repository): Extension<Arc<T>>,
    Json(payload): Json<CreateTodo>,
) -> impl IntoResponse {
    let response = match payload.text.len() {
        len if len <= 0 => (StatusCode::BAD_REQUEST, ERR_STR_EMPTY.to_string()).into_response(),
        len if len > 100 => (StatusCode::BAD_REQUEST, ERR_STR_OVER.to_string()).into_response(),
        _ => {
            let todo = repository.create(payload);
            (StatusCode::CREATED, Json(todo)).into_response()
        }
    };

    response
}

pub async fn find_todo<T: TodoRepository>(
    Path(id): Path<i32>,
    Extension(repository): Extension<Arc<T>>,
) -> Result<impl IntoResponse, StatusCode> {
    let response = match repository.find(id) {
        Some(todo) => (StatusCode::OK, Json(todo)).into_response(),
        None => (StatusCode::NOT_FOUND, ERR_STR_NOT_FOUND.to_string()).into_response(),
    };

    Ok(response)
}

pub async fn all_todo<T: TodoRepository>(
    Extension(repository): Extension<Arc<T>>,
) -> impl IntoResponse {
    let todos = repository.all();
    (StatusCode::OK, Json(todos)).into_response()
}

pub async fn update_todo<T: TodoRepository>(
    Extension(repository): Extension<Arc<T>>,
    Path(id): Path<i32>,
    Json(payload): Json<UpdateTodo>,
) -> Result<impl IntoResponse, StatusCode> {
    let response = match payload.text.as_deref().unwrap_or("").len() {
        len if len <= 0 => (StatusCode::BAD_REQUEST, ERR_STR_EMPTY.to_string()).into_response(),
        len if len > 100 => (StatusCode::BAD_REQUEST, ERR_STR_OVER.to_string()).into_response(),
        _ => match repository.update(id, payload) {
            Ok(todo) => (StatusCode::CREATED, Json(todo)).into_response(),
            Err(_) => (StatusCode::NOT_FOUND, ERR_STR_NOT_FOUND.to_string()).into_response(),
        },
    };

    Ok(response)
}

pub async fn delete_todo<T: TodoRepository>(
    Path(id): Path<i32>,
    Extension(repository): Extension<Arc<T>>,
) -> StatusCode {
    let response = match repository.delete(id) {
        Ok(_) => StatusCode::NO_CONTENT,
        Err(_) => StatusCode::NOT_FOUND,
    };

    response
}

```

※以下の記事ではaxum v0.7.5で似たようなことをしているようだったので、こちらを参考にすれば`ValidatedJson`を活かせるかもしれない（が試していない）

https://zenn.dev/codemountains/articles/159a8a0323a56f

3章までのソースコード全体はこちら

https://github.com/KMMThmurakami/rust_todo_public/tree/Chapter3

## 第4章

### Cargo.toml

- sqlx:
    - 書籍:0.14.0 → 0.8.0
https://docs.rs/sqlx/latest/sqlx/

4章で追加したのは以下

```Cargo.toml
[dependencies]
dotenv = "0.15.0"
sqlx = { version = "0.8.0", features = ["runtime-tokio-rustls", "any", "postgres" ] }

[features]
default = ["database-test"]
database-test = []
```

sqlxでは特にバージョンの違いで詰まることはなかった。
４章までのソースコード全体はこちら

https://github.com/KMMThmurakami/rust_todo_public/tree/Chapter4

## 第5章

node, npmのバージョンは以下の通り
```
$ node -v
v18.12.1

$ npm -v
8.19.2
```

### Cargo.toml

- tower-http:
    - 書籍:0.2.5 → 0.5.2
https://docs.rs/tower-http/0.5.2/tower_http/

5章で追加したのは以下

```Cargo.toml
[dependencies]
tower-http = { version = "0.5.2", features = ["cors"] }
```

### 5.4 外部APIとの通信(1)

`tower_http::cors::Origin`がなくなっており、tower-http v0.5.2では代わりに`tower_http::cors::AllowOrigin`になっていた。

```rust:main.rs
use tower_http::cors::{Any, CorsLayer, AllowOrigin};

/* 省略 */

fn create_app<T: TodoRepository>(repository: T) -> Router {
    Router::new()
        .route("/", get(root))
        .route("/todos", post(create_todo::<T>).get(all_todo::<T>))
        .route(
            "/todos/:id",
            get(find_todo::<T>)
                .delete(delete_todo::<T>)
                .patch(update_todo::<T>),
        )
        .layer(Extension(Arc::new(repository)))
        .layer(
            CorsLayer::new()
            .allow_origin(AllowOrigin::exact("http://localhost:3001".parse().unwrap()))
            .allow_methods(Any)
            .allow_headers(vec![CONTENT_TYPE]),
        )
}
```

ただ、これだけだとhttp://127.0.0.1:3001 からアクセスした場合にCORSエラーで登録ができない。

`AllowOrigin::list`を使うとCORSを許可したいURLのリストをベクタで持つことができた。

```rust:main.rs
fn create_app<T: TodoRepository>(repository: T) -> Router {
    let allowed_origins = vec![
        "http://localhost:3001".parse().unwrap(),
        "http://127.0.0.1:3001".parse().unwrap(),
    ];

    let cors = CorsLayer::new()
        .allow_origin(AllowOrigin::list(allowed_origins))
        .allow_methods(Any)
        .allow_headers(vec![CONTENT_TYPE]);

    Router::new()
        .route("/", get(root))
        .route("/todos", post(create_todo::<T>).get(all_todo::<T>))
        .route(
            "/todos/:id",
            get(find_todo::<T>)
                .delete(delete_todo::<T>)
                .patch(update_todo::<T>),
        )
        .layer(Extension(Arc::new(repository)))
        .layer(cors)
}
```

5章までのソースコードはこちら

https://github.com/KMMThmurakami/rust_todo_public/tree/Chapter5

## 第6章

### 6.1 ラベルのCRUD
#### handlerの追加
> ValidatedJsonをhandlers.rsへ移動しましょう。

とあるが、[3.6 バリデーションの追加](https://qiita.com/KMMT_h_m/private/1d7a9dccafa973812ed5#36-%E3%83%90%E3%83%AA%E3%83%87%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E3%81%AE%E8%BF%BD%E5%8A%A0)でValidatedJson型を作らず、ハンドラの中で自力でバリデーションするようにしたので、移動するものはなかった。

また、バージョン違いによるエラーとは関係ないが、TodoとLabelで書籍の中で若干コードの構造が異なるところがあったので少し手間取った。
その際のコミットは[こちら](https://github.com/KMMThmurakami/rust_todo_public/commit/8926a88813f1684feceb0c089e0e4b870eff112b)

### 6.2 TodoRepositoryのラベル対応
書籍と最新バージョンの違いで実装に悩むところはなかったが、ちょっとしたタイポなどでテストが通らず苦労した。
※Amazonのレビューなどにもあるが、6章から「あとは自分で調べて実装してみましょう」みたいな~~端折ってある~~演習形式の部分が出てくる

コードは後でまとめて記載。

### 6.3 ラベル機能を画面に追加する

Rustのコード変更はなく、Reactの修正のみ。
これで一通り書籍の内容は完了。

https://github.com/KMMThmurakami/rust_todo_public/tree/Chapter6

### 6.3_ex 一部追加で修正

書籍の内容は一応すべて完了したのだが、今のままでは少しバグが残っている。

- todoにlabelが紐づけられている場合、todo自体が削除できない
- todoにlabelが紐づけられている場合、紐づいているlabelが削除できない

1つずつ見ていく。

#### labelが紐づいているtodoの削除
これはDBの外部キー制約によって、todo_labelsテーブルのカラムが削除できないためエラーになる。
`todo_id`に`ON DELETE CASCADE`をつけるとlabelの紐づきがあってもtodoを削除できるようになる。
※一度DB設定をリセットしないとマイグレーションファイルとDBの状態が一致せずにエラーになる

```
CREATE TABLE todo_labels
(
    id       SERIAL PRIMARY KEY,
    todo_id  INTEGER NOT NULL REFERENCES todos (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
    label_id INTEGER NOT NULL REFERENCES labels (id) DEFERRABLE INITIALLY DEFERRED
);

```

#### todoに紐づいているlabelを削除しようとした場合のエラー表示
同じ理由でtodoに紐づいているlabelもエラーになるが、こちらはtodoとの紐づきがなくなるまで削除させない仕様としたい。
このとき、label削除に失敗するが、コンソールエラーが出るだけで画面上では削除に失敗していることが分からない。
書籍にも「自分なりにエラーメッセージの表出ロジックやUIの検討をしてみましょう」と記載があるので、取り組んでみた。

UIとしてはお粗末ではあるが、とりあえずlabel削除時のモーダルにエラーメッセージを出すようにした。
表出ロジックも単純で、label削除の状態をuseStateで管理し、DELETEリクエストで失敗が返ってきたらメッセージを出す、というだけ。
※もっと他にいい方法があったら教えてください

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/487953/04936fc7-2386-a35f-d5c5-40a5496d0920.png)

ここまでのコードはこちら。

https://github.com/KMMThmurakami/rust_todo_public/tree/Chapter6_custom

## 感想

「Rustを学びたい、Rustで何か作ってみたい」というところから、書籍のバージョンより新しいものがあるならやってみようという感じで初めてみたが、まずRustの日本語の解説記事や資料が2024年現在でもまだ豊富でなく、エラー解消に苦労した。(一部はChatGPTに頼ったりもしました)
各クレートに関してはとにかくひらすらドキュメントを読むしかない。

また、8割ぐらいは書籍の内容通りなのと、どちらかというとRustそのものではなくRustの"axum"、"sqlx"の使い方を学んだという感じなので、このアプリケーション制作を通してRustへの理解が深まったかというとあまりそうではないかもしれない。（Rustのコードに慣れる、というところは達成できたかも）

次にやりたいこと
- [本番環境にデプロイ (更新：次の記事でデプロイについて記載)](https://qiita.com/KMMT_h_m/private/fe7817ed5409c951699e)
- オリジナル機能追加
- フロントエンドもRustで実装（[Dioxus](https://dioxuslabs.com/)など）
など...

## 派生記事リンク
https://qiita.com/KMMT_h_m/private/fe7817ed5409c951699e

https://qiita.com/KMMT_h_m/private/33df79eaed29dd1a57a2

https://qiita.com/KMMT_h_m/private/10616daa60ac98bf2740