LoginSignup
4
2

【入門】 Rust・Axumを使ったCRUD操作を行うWeb APIの作成

Last updated at Posted at 2024-02-19

はじめに

Rustに入門したいという思いがあり、CRUD操作を行うAPIでも作ってみようと作成しました。

自分が辿った手順をまとめ、自身の忘備録とこれから学習される方の参考になればと思います。

こちらの記事では、データベースとの接続は行っていません。
リクエストされたHTTPメソッドに応じて、ファイル内に定義された変数に対してCRUD操作を行います。

前提条件

・Rustのインストール
Cargoを利用できる状態である必要があります。

・Rustの基本的な知識
記事内でも簡単な解説は行いますが、基本的な知識があれば理解が捗ると思います。
Rust ツアー公式ドキュメントが参考になると思います。

・VScodeの利用
便利な拡張機能があるので利用します。

・Postmanの利用
作成した関数が目的通りの動作をしているか確認するために利用します。
簡単な利用法は以下の記事も参考になると思います。

便利なVScode拡張機能

VScodeで利用することができるRustの便利な拡張機能を紹介しようと思います。

rust-analyzer

公式が作成している拡張機能です。多くのサポートをしてくれるので入れておきます。

Vscodeのsetting.jsonに以下を追加することで、フォーマッタとして有効にすることができます。

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] に以下を追記してください。

Cargo.toml
[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:3000Getでアクセスすると、Hello Worldとレスポンスされています。

スクリーンショット 2024-02-18 16.43.14.png

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で確認します。

スクリーンショット 2024-02-18 21.02.32.png

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で確認します。

スクリーンショット 2024-02-19 15.02.11.png

"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で確認しておきましょう。

スクリーンショット 2024-02-19 16.01.50.png

更新された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英単語帳を運営しています!

ぜひ興味を少しでも持っていただいた方は見てやってください🙇

4
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
2