Posted on 

Rust + Axum + Shuttle でAPIサーバ構築

RustでAPIサーバを作成し、デプロイしてみたので共有します。

概要

開発環境
  • MacbookAir(M1チップ)
    • Ventura 13.0
  • Rust
    • cargo 1.67.1 (8ecd4f20a 2023-01-10)

webサービスのフロントエンドを開発するにあたり、ただデータを返してくれるだけのモックサーバが必要になったため、勉強も兼ねてRustでAPIサーバを建ててみることにしました。

APIの設計

pdfファイルを管理するアプリなので、pdfの一覧と詳細を取得できれば良いです。

  • /list:pdfのリストを返します
コマンド
1
$ curl https://nu-wiki-mock-pdf-detail.shuttleapp.rs/list
結果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
[
{
"file_id": 0,
"name": "微分積分学1"
},
{
"file_id": 1,
"name": "線形代数学"
},
{
"file_id": 2,
"name": "システム数学及び演習1"
},
{
"file_id": 3,
"name": "シミュレーション"
},
{
"file_id": 4,
"name": "物理基礎2"
}
]
  • /detail/:file_id:与えられたidを持つpdfファイルの詳細情報を返します
コマンド
1
$ curl https://nu-wiki-mock-pdf-detail.shuttleapp.rs/detail/0
結果
1
2
3
4
5
{
"file_id": 0,
"name": "微分積分学1",
"url": "https://www.nagoya-u.ac.jp/academics/upload_images/5b2f064da9816cd6192d25c3a6d262ae_1.pdf"
}

実装

サーバの設計

リクエストを処理するフローは以下の通り。(下図参照)

  1. jsonファイルを配置
  2. データをデータベースに保存
  3. オブジェクトをjsonとして返す

ここからは、各段階を詳しく説明します。


1.jsonファイルを配置

ファイル構成

ファイル構成はこんな感じにしました。

1
2
3
4
5
6
7
8
9
nu-wiki-mock-pdf-detail
├ src
│ ├ lib.rs # ルーティングを行う
│ ├ pdf_list.rs # pdf_listをjsonから読み込む
│ └ pdf_detail.rs # pdf_detailをjsonから読み込む
├ static
│ ├ pdf_list.json # pdf_listの中身
│ └ pdf_detail.json # pdf_detailの中身
└ Cargo.toml

staticディレクトリのマウント

jsonファイルを/staticにおいただけでは存在が認識されないため、shuttleのライブラリを使ってマウントします。

src/lib.rs
1
2
3
4
5
6
7
8
9
10
11
use shuttle_static_folder::StaticFolder;

#[shuttle_service::main]
async fn axum(
#[StaticFolder(folder = "static")] static_folder: PathBuf,
) -> shuttle_service::ShuttleAxum {
// ...

// ルーティングを行う関数(実質main関数)
// この中では `static_folder` が `/static` として使える
}

2.データをデータベースに保存

jsonの読み込み

Rustでjsonの操作を行うライブラリserdeを利用しました。jsonと同じ形式の構造体を作ることで、簡単に json⇄Rustのオブジェクト ができるので便利です。

データベースに追加

データベースと言ってはいますが、SQLなどのリレーショナルデータベースではなく単なるグローバル変数です。 Rustではただグローバルな領域に変数を置いただけではグローバル変数にならないため、Arcという型に包みます。 Arcはスマートポインタであり、 自分が参照された回数をカウントしながら安全な形で同一のデータにアクセスする仕組みです。 また、Arcはデータへの参照を保存するだけで、データの読み込みや書き込みには対応していないため、 RwLockという型で包むことで、複数のスレッドからも安全にデータの読み書きをすることができます。

1
2
3
4
5
6
7
8
9
+------- Arc --------+
| | ← データを保存する箱
| +--- RwLock ---+ |
| | | ← 安全に読み書きできるようにする箱
| | [ Data ] | |
| | | |
| +--------------+ |
| |
+--------------------+

詳細は以下の記事を参照してください。

3.オブジェクトをjsonとして返す

ルーティング

ルーティングはlib.rsaxum関数内で行います。 axum::Routerオブジェクトに、メソッドチェーンの形でroute関数を追加していけばいいだけなのでかなり簡単です。

src/lib.rs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
use axum::Router;

#[shuttle_service::main]
async fn axum() -> shuttle_service::ShuttleAxum {
// ...

// appのルーティングを定義
let app = Router::new()
.route("/list", get(get_list)) // "/list" にリクエストが来たとき
.with_state(db_list) // → db_listを用いる
.route("/detail/:id", get(get_detail)) // "/detail/:id" にリクエストが来たとき
.with_state(db_detail); // → db_detailを用いる

// ...
}

リクエストの処理

上のコードの9行目では/listへのGETリクエストの時にget_list関数を呼び出すことを定義しています。 このget_list関数は下のように定義されています。 引数は上のコードの10行目のwith_stateメソッドで付加されたdb_listStateに包まれた状態で渡されます。 全て説明すると大変なので省略しますが、いくつものレイヤーで包まれたデータをメソッドチェーンで剥いて行って、 最終的にJsonオブジェクトとして返すという感じです。

src/lib.rs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// dbの定義
type DbPdfList = Arc<RwLock<Vec<PdfOverview>>>;

/// ## get_list
/// pdfの一覧を返す
async fn get_list(State(db): State<DbPdfList>) -> Json<Vec<PdfOverview>> {
let list = db // DbPdfList = Arc<RwLock<Vec<PdfOverview>>>
.read() // → Result<RwLockReadGuard<Vec<PdfOverview>>>
.unwrap() // → RwLockReadGuard<Vec<PdfOverview>>
.deref() // → &Vec<PdfOverview>
.clone(); // → Vec<PdfOverview>

Json(list)
}

デプロイ

Shuttleという、Rust製のwebアプリを無料でデプロイすることができるサービスを利用しました。 サーバ側の設定などは必要なく、コマンドのみでデプロイできるのは非常に快適でした。

コマンドの概要
1
2
3
$ cargo shuttle init         # shuttleの初期化
$ cargo shuttle project new # プロジェクトの作成
$ cargo shuttle deploy # デプロイ🚀

今回は試しませんでしたが、データベースへの接続なども無料でできるみたいです👍

詳細は公式のドキュメントやexampleを参照してみてください。

終わりに

今回初めて、Rustでwebアプリをデプロイするところまで挑戦してみました。 よく言われる通り、Rustでのアプリ開発はかなり難しいですが 厳格な型による安心感と保守のしやすさというメリットは圧倒的だなと感じました。

また、shuttleはまだ発展途上だなと思うような場面にも何度か遭遇しましたが、 開発コミュニティのDiscordがかなり活発で、質問を投げるとすぐに返してくれたことも 非常にありがたかったです。

この記事を読んでRustによるweb開発に興味を持ってくださった皆さん、 一緒にRustを勉強してみませんか?


このページはHexoStellarを使用して作成されました。