🦍

Rust | Axum の axum-valid を使ってバリデーションを実装する

2024/10/12に公開
2

はじめに

本記事では Rust の Axum という Webフレームワークを使ってクエリー、ボディのバリデーションを実装してみたいと思います!

Axum のバリデーションには、axum-valid と呼ばれるクレートが存在しますので、今回はそちらを使ってバリデーションのロジックを書いていきます。

意外にも紹介している人が少なめかつ、本クレートを利用することで非常に便利なバリデーションを実施することができますので、ぜひ、手元でも動かしてもらえると理解しやすいと思います。

Axum のプロジェクトを作成する

まずは Axum のプロジェクトを作ります。

$ cargo new axum-web
$ cd axum-web

cargo runを実行して、"Hello, World!"できていれば問題ないです。

Axum の必要なクレートを追加

以下のコマンドで Axum の実装に必要な最低限のクレートが追加できます。

$ cargo add axum
$ cargo add tokio --features full

これで下準備は完了です✨

axum-valid の Valid<E> で API サーバーのクエリー、ボディにバリデーションを仕掛ける

本題の axum-valid を使って、バリデーションを走らせてみます。
axum-valid にはさまざまなバリデーションの方法がありますが、
一番シンプルな Valid<E>を利用していきます!

axum-valid などのクレートを追加する

まずは、クレートの準備です。
以下のコマンドでクレートを追加します。

axum-valid と一緒に validator というクレートも必要になってきますので、追加します。
また、serde クレートの追加も行います。

$ cargo add validator --features derive
$ cargo add axum-valid

$ cargo add serde --features derive

バリデーションロジックを記述していく

GET /users、POST /usersというシンプルなエンドポイントを2種類用意しています。

main.rs
use axum::{extract::Query, http::StatusCode, response::IntoResponse, routing::get, Json, Router};
use axum_valid::Valid;
use serde::{Deserialize, Serialize};
use validator::Validate;

#[derive(Validate, Serialize, Deserialize)]
pub struct User {
    #[validate(range(min = 1, max = 150, message = "Age must be between 1 and 150"))]
    pub age: u16,

    #[validate(length(
        min = 2,
        max = 50,
        message = "Name must be between 3 and 50 characters"
    ))]
    pub name: String,
}

pub async fn fetch_user(
    Valid(Query(query)): Valid<Query<User>>,
) -> Result<impl IntoResponse, StatusCode> {
    assert!((1..=150).contains(&query.age));
    assert!((2..=50).contains(&query.name.chars().count()));

    let user = vec![{
        User {
            age: query.age,
            name: query.name,
        }
    }];

    Ok((StatusCode::OK, Json(user)))
}

pub async fn create_user(
    Valid(Json(payload)): Valid<Json<User>>,
) -> Result<impl IntoResponse, StatusCode> {
    assert!((1..=150).contains(&payload.age));
    assert!((2..=50).contains(&payload.name.chars().count()));

    let user = {
        User {
            age: payload.age,
            name: payload.name,
        }
    };

    Ok((StatusCode::CREATED, Json(user)))
}

#[tokio::main]
async fn main() {
    let app = Router::new().route("/users", get(fetch_user).post(create_user));

    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
    axum::serve(listener, app).await.unwrap();
}

上記の例では、User構造体を定義していて、/users にその構造体をそのまま返すというシンプルなソースコードになっています。
axum-valid を使う上でポイントになるところは、以下のような点かなと思います。

  • 構造体のパラメーター上部に #[validate()] を追加すること
  • ハンドラー関数の引数に仕掛けを用意しておくこと
    • Valid(Json(payload)): Valid<Json<User>>, の部分

ポイントを押さえた上でも、
使用感が良く、コードの視認性にも影響が少ない点を踏まえても、
使い勝手が非常に優れているのではないでしょうか。

おわりに

ざっくりですが、axum-valid の紹介でした!
バリデーションの下限値や上限値をソースコード上でも表現しやすくなっているところが作り込まれている印象ですね。ぜひ、皆さんも使ってみてはいかがでしょうか。

ではでは!

PS. 最近、ありがたいことに記事を読んでくださる人が増えてきたように感じます。最後まで読んでいただきありがとうございます!!😋

コラボスタイル Developers

Discussion

kanaruskanarus

パラメータ類のバリでションチェック
( はじめに )

typo ( バリでション → バリデーション )

もっと言うと
  • 「バリデーションチェック」って普通言うでしょうか?そういう文化圏があるなら申し訳ないのですが、普通は単に「バリデーション」と言いませんか?
  • この記事ではリクエストボディのバリデーションもやってるので「パラメータ類の」というのはやや誤解を招く説明だと思います

assert!((1..=50).contains〜
( create_user )

assert!((1..=150).contains〜 のタイポですかね?


assert!()などを使用してエラーをハンドリングする必要がある

エラーをハンドルしているのは Valid<T>FromRequest 実装であって、この assert!axum_valid::Valid の説明にあたって「ハンドラーにはバリデーション済の Valid<T> が渡される」ことを分かりやすく示す手段として採用されているだけでは?

ShotaShota

kanarus さん

コメントいただきありがとうございます!

タイポが大変多く、失礼しました。

普通は単に「バリデーション」と言いませんか?

バリデーションの意味を再度確認してみたのですが、「確認」や「検証」あるいは「妥当性の確認」の意味であることから、「バリデーションチェック」とすると「頭痛が痛い」みたいな表現になってしまいそうですね。
「普通」が「一般的」と同義とすると「バリデーション」かなと思います!

エラーをハンドルしているのは Valid<T> の FromRequest 実装であって、この assert! は axum_valid::Valid の説明にあたって「ハンドラーにはバリデーション済の Valid<T> が渡される」ことを分かりやすく示す手段として採用されているだけでは?

おっしゃる通り、ドキュメントがわかりやすく表現されているだけでした。
誤解のないよう、適切な説明に修正します!