RustでCloudflare D1からデータを取得するAPIを実装し、Vue.jsのアプリでAPIからデータを取得した話

# この記事は？
RustでCloudflare D1からデータを取得するAPIを実装し、Vue.jsのアプリでAPIからデータを取得するということをしたので、その紹介です。

解説ではなく、やったこと紹介です。

# やったこと
- RustでCloudflare D1からデータを取得するAPIを実装し、Cloudflare Workersで動かす（←適切な表現かはわからない）
- Vue.jsのアプリでAPIからデータ取得
- CORSエラー対応

# 導入
RustでAPIを実装できるらしいのでやってみました。
なんとなくCloudflare D1を使い、かつ、APIをたたくのはなんでもよいのですが、今後の予定からVue.jsを使用しました。
よくわからないけど、CORSエラーが起こらないように実装しました。

# 流れ
APIを実装してから、アプリを作成しました。
するとCORSエラーが発生しましたので、API側にCORS対応をおこなったという流れです。
以下でもその順番で実施したことを紹介します。

# 本編
## RustでCloudflare D1からデータを取得するAPIを実装し、Cloudflare Workersで動かす

Rustの開発環境は導入済みなところから始めます。
まずは以下でHello Worldなスケルトンをつくります。

```
cargo generate cloudflare/workers-rs
```
↓　実行結果
```
Favorite `cloudflare/workers-rs` not found in config, using it as a git repository: https://github.com/cloudflare/workers-rs.git
✔  Which template should be expanded? · templates\hello-world
Project Name: server_rust
Destination: E:\****\server_rust ...
project-name: server_rust ...
Generating template ...
Moving generated files into: `E:\\****\\server_rust`...

Initializing a fresh Git repository
Done! New project created E:\****\server_rust
```

```
cd ./server_rust
npx wrangler dev
```
これで、Hello Worldが表示されます。
と思ったら、エラーになります。

```
   Compiling worker v0.2.0                                                
error[E0599]: no method named `unwrap` found for struct `js_sys::Iterator` in the current scope
  --> ****\.cargo\registry\src\index.crates.io-6f17d22bba15001f\worker-0.2.0\src\headers.rs:83:14
   |
80 | / ...   self.0
81 | | ...       .keys()
82 | | ...       // Header.keys() doesn't error: https://developer.moz... 
83 | | ...       .unwrap()
   | |           -^^^^^^ method not found in `Iterator`
   | |___________|
   | 
```


<details><summary>エラー全文（長いので折りたたみ）</summary>

```
   Compiling worker v0.2.0                                                
error[E0599]: no method named `unwrap` found for struct `js_sys::Iterator` in the current scope
  --> ****\.cargo\registry\src\index.crates.io-6f17d22bba15001f\worker-0.2.0\src\headers.rs:83:14
   |
80 | / ...   self.0
81 | | ...       .keys()
82 | | ...       // Header.keys() doesn't error: https://developer.moz... 
83 | | ...       .unwrap()
   | |           -^^^^^^ method not found in `Iterator`
   | |___________|
   | 

error[E0599]: no method named `unwrap` found for struct `js_sys::Iterator` in the current scope
  --> ****\.cargo\registry\src\index.crates.io-6f17d22bba15001f\worker-0.2.0\src\headers.rs:95:14
   |
92 | / ...   self.0
93 | | ...       .values()
94 | | ...       // Header.values() doesn't error: https://developer.m... 
95 | | ...       .unwrap()
   | |           -^^^^^^ method not found in `Iterator`
   | |___________|
   | 

error[E0599]: no method named `unwrap` found for struct `js_sys::Iterator` in the current scope
  --> ****\.cargo\registry\src\index.crates.io-6f17d22bba15001f\worker-0.2.0\src\headers.rs:69:14
   |
66 | / ...   self.0
67 | | ...       .entries()
68 | | ...       // Header.entries() doesn't error: https://developer.... 
69 | | ...       .unwrap()
   | |           -^^^^^^ method not found in `Iterator`
   | |___________|
   | 

For more information about this error, try `rustc --explain E0599`.
error: could not compile `worker` (lib) due to 3 previous errors
Error: Compiling your crate to WebAssembly failed
Caused by: Compiling your crate to WebAssembly failed
Caused by: failed to execute `cargo build`: exited with exit code: 101    
  full command: "cargo" "build" "--lib" "--release" "--target" "wasm32-unknown-unknown"
Error: wasm-pack exited with status exit code: 1

X [ERROR] Running custom build `cargo install -q worker-build && worker-build --release` failed. There are likely more logs from your build command above.
```
</details>

そのままビルドできないなんてけしからんですね。
プンプン怒りながら、ググります。

https://github.com/cloudflare/workers-rs/issues/617#issuecomment-2287137829

Workaroundが提示されていたので、`Cargo.toml`に追記し回避します。

```
[dependencies]
wasm-bindgen = "=0.2.92"
```

何をしているか不明ですが、ビルドに成功し、動きました。
Hello Worldさん、こんにちは。

今回はCloudflare D1からデータを取得するため、`Cargo.toml`を以下のように修正します。
ついでに使うので、`serde_json`と`serde`も追加します。

```diff:Cargo.toml
[dependencies]
- worker = { version="0.2.0" }
+ worker = { version="0.2.0", features=["d1"] }
+ serde_json = "1.0.125"
+ serde = "1.0.208"
```

また、`wrangler.toml`にDB情報も記載します。

```toml:wrangler.toml
[[d1_databases]]
binding = "DB" # i.e. available in your Worker on env.DB
database_name = "workers-rs-d1"
database_id = "****"
```

データベースのスキーマも準備します。
内容はなんでもいいので、ゲームの素材情報です。

```sql:db/schema.sql
DROP TABLE IF EXISTS materials;
CREATE TABLE materials
(id TEXT, name TEXT, category TEXT, PRIMARY KEY (`id`));

INSERT INTO materials (id, name, category) VALUES
("e8c40da556a", "ウコギ原木", "木材"),
("46f892dc3ba", "オルコクロマイト", "石材"),
("364a2350c5e", "雪木綿", "布材"),
("8f817bd6b10", "セイバ原木", "木材");
```

これを実行します。今回はローカルのデータベースでのみおこないます。

```
npx wrangler d1 execute workers-rs-d1 --local --file=./db/schema.sql
```

正しくデータが入っているかを確認します。
```
npx wrangler d1 execute workers-rs-d1 --local --command="SELECT * FROM materials"
```
↓　実行結果
```
 ⛅️ wrangler 3.65.1 (update available 3.72.2)
-------------------------------------------------------

🌀 Executing on local database workers-rs-d1 (****) from .wrangler\state\v3\d1:
🌀 To execute on your remote database, add a --remote flag to your wrangler command.
┌─────────────┬──────────┬──────────┐
│ id          │ name     │ category │
├─────────────┼──────────┼──────────┤
│ e8c40da556a │ ウコギ原木    │ 木材       │
├─────────────┼──────────┼──────────┤
│ 46f892dc3ba │ オルコクロマイト │ 石材       │
├─────────────┼──────────┼──────────┤
│ 364a2350c5e │ 雪木綿      │ 布材       │
├─────────────┼──────────┼──────────┤
│ 8f817bd6b10 │ セイバ原木    │ 木材       │
└─────────────┴──────────┴──────────┘
```

よさそうです。

最後に本体の処理の記述です。

```rs:src/lib.rs
use worker::*;
use serde::{Serialize, Deserialize};

#[derive(Serialize, Deserialize, Debug)]
struct Materials {
    id: String,
    name: String,
    category: String,
}

#[event(fetch)]
async fn fetch(
    req: Request,
    env: Env,
    _ctx: Context,
) -> Result<Response> {
    console_error_panic_hook::set_once();
    Router::new()
        .get_async("/", |_, ctx | async move {
            let d1 = ctx.env.d1("DB")?;
            let statement = d1.prepare("select * from materials");
            let result = statement.all().await?;
            Response::from_json(&result.results::<Materials>().unwrap())
        })
        .run(req, env)
        .await
}
```

ソースコードは以下を参考にさせていただきました。
ありがとうございます。

https://dev.classmethod.jp/articles/workers-rs-d1/

これを動かして、WebブラウザからURLへ直接アクセスすると以下が表示されました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3821654/3d183461-be17-3509-f9bf-0b07309830a9.png)

ひとまずOKです。

## Vue.jsのアプリでAPIからデータ取得

https://vuejs.org/guide/quick-start

いつもどおりにプロジェクトを作成します。

```
npm create vue@latest
```

そして、`src/App.vue`を書き換えます。

```vue:src/App.vue
<script setup>
import { ref, onMounted } from 'vue';

const data = ref();

onMounted(async () => {
  const response = await fetch(`http://127.0.0.1:8787`, {
    cache: 'no-store',
  });
  data.value = await response.json();
});
</script>

<template>
  <div class="fl">
    <div class="ma" v-for="material in data">
      <p>id: {{ material.id }}</p>
      <p>name: {{ material.name }}</p>
      <p>category: {{ material.category }}</p>
    </div>
  </div>
</template>

<style scoped>
div.fl {
  display: grid;
  grid-template-columns: 1fr 1fr 1fr 1fr;
}

div.ma {
  margin-right: 2rem;
  margin-bottom: 2rem;
}

p {
  margin-bottom: .6rem;
}
</style>
```

動かします。

白紙のページが表示されました。
DevToolからコンソールを見ると、CORSポリシー違反が発生していました。

```
Access to fetch at 'http://127.0.0.1:8787/' from origin 'http://localhost:5173' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3821654/621a016a-778c-79d3-ba67-38b94c31d67c.png)

ということで、対策します。

## CORSエラー対応

ぐぐったら、以下が出ましたので、これを参考に実装します。

https://github.com/cloudflare/workers-rs/issues/364

```diff:src/lib.rs
use worker::*;
use serde::{Serialize, Deserialize};

#[derive(Serialize, Deserialize, Debug)]
struct Materials {
    id: String,
    name: String,
    category: String,
}

#[event(fetch)]
async fn fetch(
    req: Request,
    env: Env,
    _ctx: Context,
) -> Result<Response> {
    console_error_panic_hook::set_once();

+    let cors = Cors::default().with_origins(vec!["*"]);

    Router::new()
        .get_async("/", |_, ctx | async move {
            let d1 = ctx.env.d1("DB")?;
            let statement = d1.prepare("select * from materials");
            let result = statement.all().await?;
            Response::from_json(&result.results::<Materials>().unwrap())
        })
        .run(req, env)
-        .await
+        .await?
+        .with_cors(&cors)
}
```

これでもう一度確認します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3821654/ab8c22c9-3d4b-0960-28c9-f42510954e9d.png)

表示されました。
課題クリアです。


# まとめ

ということで、Rustでサーバーサイドを書くことができました。

CORS周りやAPIの実装方法にたどり着くまでは少し大変でしたが、実装できて満足です。

後は、ローカル開発環境とプロダクション環境で、APIのURLが変わるのを環境変数で切り分けると、実際にCloudflare Workersにデプロイしても動くはずです。
（この例ではおこなっていませんが、他に実施したものでは切り分けができました）

いつかこの仕組みで、自分用にゲームの素材計算機をつくりたいなあとぼんやり考えています。

では、また何か記事にしたいことができましたら。