はじめに
Rustに入門したいという思いがあり、CRUD操作を行うAPIでも作ってみようと作成しました。
自分が辿った手順をまとめ、自身の忘備録とこれから学習される方の参考になればと思います。
こちらの記事では、データベースとの接続は行っていません。
リクエストされたHTTPメソッドに応じて、ファイル内に定義された変数に対してCRUD操作を行います。
前提条件
・Rustのインストール
Cargoを利用できる状態である必要があります。
・Rustの基本的な知識
記事内でも簡単な解説は行いますが、基本的な知識があれば理解が捗ると思います。
Rust ツアー や 公式ドキュメントが参考になると思います。
・VScodeの利用
便利な拡張機能があるので利用します。
・Postmanの利用
作成した関数が目的通りの動作をしているか確認するために利用します。
簡単な利用法は以下の記事も参考になると思います。
便利なVScode拡張機能
VScodeで利用することができるRustの便利な拡張機能を紹介しようと思います。
rust-analyzer
公式が作成している拡張機能です。多くのサポートをしてくれるので入れておきます。
Vscodeのsetting.jsonに以下を追加することで、フォーマッタとして有効にすることができます。
"[rust]": {
"editor.defaultFormatter": "rust-lang.rust-analyzer"
},
crates
クレートのバージョン管理を簡易化してくれます。
Axumとは
公式ドキュメントには以下のように書かれています。
・マクロを含まない API を使用してリクエストをハンドラーにルーティングします。
・エクストラクターを使用してリクエストを宣言的に解析します。
・シンプルで予測可能なエラー処理モデル。
・最小限の定型文で応答を生成します。
・ミドルウェア、サービス、ユーティリティのエコシステムtowerを最大限に活用します。tower-http
前置きが長くなりましたが、次の章から実際にAPIの実装に入りたいと思います。
API の実装
実際の実装に入りたいと思います。
新規プロジェクト作成と依存関係の追加
まずターミナルにて、Rustの新規プロジェクトを作成します。
cargo new hello-rust-axum-api
作成後に
cargo run
を行うと "Hello World" と出力されることが確認できます。
依存関係の追加
Rustでは依存関係を Cargo.toml にて管理します。
ここに今回の記事で利用するクレートを追加していきます。Cargo.toml の [dependencies] に以下を追記してください。
[dependencies]
axum = "0.7.4"
tokio = { version = "1.36.0", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
これによって Axum を利用する準備が整いました。
AxumにHello World
早速、簡易的なAPIを作成したいので、main.rs を以下のように変更します。
use axum::{routing::get, Router};
#[tokio::main]
async fn main() {
// Hello Worldと返すハンドラーを定義
async fn root_handler() -> String {
"Hello World".to_string()
}
// ルートを定義
// "/"を踏むと、上で定義したroot_handlerを実行する
let app = Router::new().route("/", get(root_handler));
// 指定したポートにサーバを開く
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
ソースコードにも書いてありますが、このコードでは大きく ハンドラー(ルートを踏んだ際に呼び出される関数)の定義、ルートの定義、サーバの起動を行っています。
実際にPostmanを利用して確認します。ターミナルにて
cargo run
を行います。少し待つとサーバが起動されます。
0.0.0.0:3000にGetでアクセスすると、Hello Worldとレスポンスされています。
CRUD操作を行うAPIの作成
はじめにCRUD操作によって値を変更するUser配列を作成します。
構造体の定義
idと名前のみを持つUser という名の構造体と、そのUserを配列として持つ Users という名の構造体をそれぞれ定義します。
同時に、JSON ファイルを使ってデータの送受信を行うので、それも追加します。
// response::Jsonを追加
use axum::{extract::State, response::Json, routing::get, Router};
// 以下を追加
use serde::{Deserialize, Serialize};
// JSONファイルのやりとりを可能にする
#[derive(Clone, Deserialize, Serialize)]
// 構造体を定義
struct User {
id: u32,
name: String,
}
#[derive(Clone, Deserialize, Serialize)]
struct Users {
users: Vec<User>,
}
そして、これらの構造体を元にusers 配列を作成します。
let users = Users {
users: vec![
User {
id: 1,
name: "takashi".to_string(),
},
User {
id: 2,
name: "hitoshi".to_string(),
},
User {
id: 3,
name: "masashi".to_string(),
},
],
};
これで必要なusers配列は作成出来たので、CRUD操作を行う関数の作成を始めます。
と言いたいところですが、もう少し必要な手順が存在します。
実はこのままでは、ハンドラー内部にてusers配列にアクセスすることが出来ません。
これはRustでは非同期関数で、動的環境(関数の外部に定義された変数)を直接キャプチャすることができないためです。
これを解決するために、State を利用します。
また、排他制御を動的に行うためにmutexを利用する必要があり、それも追加します。
use std::sync::Arc;
use tokio::sync::Mutex;
// Mutexにusersを包む。MutexをArcで包むのはイディオムのようなもの
let users_state = Arc::new(Mutex::new(users));
そして、この user_state をハンドラー間で共有可能な値として指定します。
let app = Router::new()
.route("/", get(root_handler))
// 以下を追加
.with_state(users_state);
これによってハンドラー間で users を共有して利用することが可能になりました。
Readの作成
ようやくCRUD関数の作成に入ります。まず、Read を作成します。
先程指定した State を取得するために extract::State を追加します。
// extract::State を追加
use axum::{extract::State, response::Json, routing::get, Router};
そして、Router に新たな Route を追加します。
let app = Router::new()
.route("/", get(root_handler))
// 以下を追加
// "/users"にgetメソッドでリクエストするとget_usersを実行
.route("/users", get(get_users))
.with_state(users);
最後に、get_users 関数を作成します。
// Readを行う関数
// usersを取得、Json<Users> を返り値として指定
async fn get_users(State(users_state): State<Arc<Mutex<Users>>>) -> Json<Users> {
// ロック(lock)を獲得
let user_lock = users_state.lock().await;
// usersを返す
Json(user_lock.clone())
}
これによってReadを行うことが可能になりました。Postmanで確認します。
usersがJSONとして取得できていることが分かります。
Createの作成
続いて、Createを行うAPIを作成します。先程作成したRouteにPostメソッドを追加します。
let app = Router::new()
.route("/", get(root_handler))
// postメソッドを追加
.route("/users", get(get_users).post(post_user))
.with_state(users_state);
受け取るデータの構造体も定義します。idはusersの数よりひとつ多い数値を割り当てるので、名前のみを受け取ります。
#[derive(Clone, Serialize, Deserialize)]
struct CreateUser {
name: String,
}
Create関数を作成します。これはStateにあるusersと、JSONとして送られるuserデータを受け取り、更新されたusersを返します。
async fn post_user(
State(users_state): State<Arc<Mutex<Users>>>,
// bodyから受け取る値を書く
create_user: Json<CreateUser>,
) -> Json<Users> {
let mut users_lock = users_state.lock().await;
// 追加するuserデータを定義
let new_user = User {
// usersの数から+1の値をu32としてidを定義
id: (users_lock.users.len() + 1) as u32,
// bodyで受け取ったJSONのnameを取得
name: create_user.name.to_string(),
};
// usersに追加
users_lock.users.push(new_user);
// 更新されたusersを返す
Json(users_lock.clone())
}
これによってCreateを行うことが出来るようになりました。内容としては、ソースコードに書かれてあるコメントで理解できると思います。
それでは関数の動作をPostmanで確認します。
"id": 4, "name": "ningen" が追加されています。
先程作成したGetメソッドで確認しても、更新後のusersが返されると思います。
Updateの作成
Updateを行う関数は、更新したいuserのidをurlパラメータをして受け取ります。
そのため、Routerには新たなRouteを追加します。
let app = Router::new()
.route("/", get(root_handler))
.route("/users", get(get_users).post(post_user))
// 新たなRouteの追加
// ":user_id"は関数内でuser_idとして受け取ることが出来る
.route("/users/:user_id", patch(patch_user))
.with_state(users_state);
そして、Pathを受け取るためと、Patchメソッドを利用するので以下を追加します。
use axum::{
// Pathの追加
extract::{Path, State},
response::Json,
// patchの追加
routing::{get, patch},
Router,
};
それではUpdateを行うpatch_user関数を作成します。
async fn patch_user(
State(users_state): State<Arc<Mutex<Users>>>,
// URLから受け取る
Path(user_id): Path<u32>,
Json(update_user): Json<CreateUser>,
// Resultを返り値に指定
) -> Result<Json<User>, String> {
let mut users_lock = users_state.lock().await;
// findの返り値がSome(user)であった場合のみ処理を行う
if let Some(user) = users_lock.users.iter_mut().find(|user| user.id == user_id) {
// 名前を更新
user.name = update_user.name.clone();
// 値をOK()に包んで返す
return Ok(Json(user.clone()));
}
// idを持つuserが見つからなかったらErr()として返す
Err("User not found".to_string())
}
少しややこしいので解説します。
まず、この関数ではResultを返り値として指定しています。これは Ok() の場合はJson< User > を、Err() の場合は Stringのように返す値を条件によって使い分けるためです。
そして、if let Some(user) = という点は、find関数がidを持つuserの探索に成功するとSome(値) の形で値を返すため、userが見つかった場合のみ処理を行うようにするためです。
これでUpdateを行うことが出来るようになったので、Postmanで確認しておきましょう。
更新されたuserの情報が返されています。Getで確認をしても、更新が確認できます。
Deleteを作成
最後にDeleteを作成します。まず、Routerにdeleteメソッドを追加します。
let app = Router::new()
.route("/", get(root_handler))
.route("/users", get(get_users).post(post_user))
// deleteの追加
.route("/users/:user_id", patch(patch_user).delete(delete_user))
.with_state(users_state);
今までのものと変わったところは特にないので、早速delete関数を作成します。
async fn delete_user(
State(users_state): State<Arc<Mutex<Users>>>,
Path(user_id): Path<u32>,
) -> Result<Json<Users>, String> {
let mut users_lock = users_state.lock().await;
// 更新前のusersの長さを保持
let original_len = users_lock.users.len();
// retainを使って、指定したIDのユーザーを削除
users_lock.users.retain(|user| user.id != user_id);
// usersの長さが変わっていれば、削除に成功している
if users_lock.users.len() == original_len {
Err("User not found".to_string())
} else {
Ok(Json(users_lock.clone()))
}
}
特徴としては、Updateと同じようにPathを使用してuser_idを取得する点と、これもResultを返り値として指定しています。
また、削除方法に関しては複数存在しますが、今回はretainを使ってフィルタリングのようなものを行っています。
そして最後に、usersの更新前後の長さを調べることで削除が成功したか確認しています。
特に変わった操作は必要ないのでDeleteメソッドを指定して、Postmanで確認をしてみてください。
以上でCRUD操作が可能なAPIを作成することが出来ました!
最後に今回作成したコードをまとめたものを載せておきます。
details
use axum::{
extract::{Path, State},
response::Json,
routing::{get, patch},
Router,
};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use tokio::sync::Mutex;
#[tokio::main]
async fn main() {
#[derive(Clone, Deserialize, Serialize)]
struct User {
id: u32,
name: String,
}
#[derive(Clone, Deserialize, Serialize)]
struct Users {
users: Vec<User>,
}
let users = Users {
users: vec![
User {
id: 1,
name: "takashi".to_string(),
},
User {
id: 2,
name: "hitoshi".to_string(),
},
User {
id: 3,
name: "masashi".to_string(),
},
],
};
let users_state = Arc::new(Mutex::new(users));
// Hello Worldと返すハンドラーを定義
async fn root_handler() -> String {
"Hello World".to_string()
}
// Read
async fn get_users(State(users_state): State<Arc<Mutex<Users>>>) -> Json<Users> {
let user_lock = users_state.lock().await;
Json(user_lock.clone())
}
#[derive(Clone, Serialize, Deserialize)]
struct CreateUser {
name: String,
}
// Create
async fn post_user(
State(users_state): State<Arc<Mutex<Users>>>,
create_user: Json<CreateUser>,
) -> Json<Users> {
let mut users_lock = users_state.lock().await;
let new_user = User {
id: (users_lock.users.len() + 1) as u32,
name: create_user.name.to_string(),
};
users_lock.users.push(new_user);
Json(users_lock.clone())
}
// Update
async fn patch_user(
State(users_state): State<Arc<Mutex<Users>>>,
Path(user_id): Path<u32>,
Json(update_user): Json<CreateUser>,
) -> Result<Json<User>, String> {
let mut users_lock = users_state.lock().await;
if let Some(user) = users_lock.users.iter_mut().find(|user| user.id == user_id) {
user.name = update_user.name.clone();
return Ok(Json(user.clone()));
}
Err("User not found".to_string())
}
// Delete
async fn delete_user(
State(users_state): State<Arc<Mutex<Users>>>,
Path(user_id): Path<u32>,
) -> Result<Json<Users>, String> {
let mut users_lock = users_state.lock().await;
// 更新前のusersの長さを保持
let original_len = users_lock.users.len();
// retainを使って、指定したIDのユーザーを削除
users_lock.users.retain(|user| user.id != user_id);
// usersの長さが変わっていれば、削除に成功している
if users_lock.users.len() == original_len {
Err("User not found".to_string())
} else {
Ok(Json(users_lock.clone()))
}
}
// Router
let app = Router::new()
.route("/", get(root_handler))
.route("/users", get(get_users).post(post_user))
.route("/users/:user_id", patch(patch_user).delete(delete_user))
.with_state(users_state);
// サーバの起動
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
終わりに
CRUD操作を行う関数を作成するだけで、記事・コード共になかなか長くなってしまいました。
実際にディレクトリ構成を考える場合はnestなんかの機能があるので、これによってrouterを分割したりすると良いかもしれません。
また、今回の記事ではデータベースを利用しませんでしたが、dieselというORMもあるので、それを使って実際にデータベースに接続をした記事を作成するのも良いなと感じました。
CRUD関数を作成しただけではありますが、Rust・Axumの基礎的な部分については理解が深まったと感じますし、この記事がそのように役に立つことがあれば嬉しいです。
宣伝
Konwalk(コンウォーク) という 「歩く時間に英単語を覚える」 をコンセプトにしたWeb英単語帳を運営しています!
ぜひ興味を少しでも持っていただいた方は見てやってください🙇