こんにちは
フロントエンドエンジニアのみつです。
先日、権限管理に関する実装の中でバリデーションについてとても悩みました。
その中で、「フロントエンドのバリデーションは最低限の方がいいらしい。」ということを学んだので、簡単なものに例えながら、理解を深めるための備忘メモです。
今日は、八百屋さんで果物を管理したいと思っています。
:りんご
:桃
:メロン
:さくらんぼ
:パイナップル
の5種類があり、商品の種類をまずは登録していきたいです。
既に登録された果物の種類は、新たにDBに登録しなくて良いので重複チェックは入れたいところです。
ただどこまでの「バリデーション」を、フロントエンドで実装し、バックエンドで実装するのかで迷いました。
⇓
バリデーションについては、テックリードにも相談し、
- 複雑な判断が必要になるケースはバックエンド側で実装するのが良さそう
- また、今回のようなケースでは、値を知っているのはDBのみという理由もありフロントエンド側では実装なし。
という方向で進めることにしました。
目次
- 考慮する必要があるポイント
- 疑問との取り扱い検討
- A:フロントエンドとバックエンドでそれぞれ重複を判断するパターン
- B:バックエンドを軸に果物の重複を判断するパターン
- テックリードにアドバイスをもらいました
- 最初に書いていた書き方
- アドバイスをもらった後の書き方
- まとめ
考慮する必要があるポイント
- 重複した登録は、許さないようにすること
- 複数の環境から同時に追加される可能性も考えること
疑問との取り扱い検討
果物の種類をDBに登録時、重複が起きる場合、理想のふるまいはどれなのか?
つまり、フロントエンド側かバックエンド側のどちらで重複チェックを実装すべきなのか??
2つ目の「:メロン」を登録する場合で考えてみます。
自分で考えた2パターンは下記でした。
A:フロントエンドとバックエンドでそれぞれ重複を判断するパターン
フロントエンド (画面の表示に使うための値を見て重複チェック)
APIレスポンスの値であり、画面表示で使うfruits配列に、「:メロン」が含まれているか次第で、含まれている場合はその場でリターン&エラートーストを出す。
バックエンド (DBのfruitsを見て重複判断)
POSTで渡ってくる「:メロン」がDBにあるかを見て、既にあれば、return the fruit is already...的なエラー
B:バックエンドを軸に果物の重複を判断するパターン
フロントエンド(返ってくるレスポンスで重複判断)
フロントエンドに渡ってきた配列を使用することでチェックできるが、しない。
ユーザーのボタン押下を起点に、フロントエンドからバックエンドに「:メロン」が送られる。
すでにDBには登録があり、エラーが返るのでそれをキャッチしてエラー
バックエンド(DBのfruitsを見て重複判断)
POSTで渡ってくる「:メロン」がDBにあるかを見て、既にあれば、return the fruit is already...的なエラー
テックリードにアドバイスをもらいました
Bを推します。
- フロントエンドでは最低限のバリデーションに留めておいたほうが良い。
- 今回のケースでは、データが重複していることを判断できるのはDBしかいない。
とのことでした。
実際に聞いた内容は下のものになるんですが、大大納得するしかない内容です。
まず前提として、名前が重複しているかどうかはデータベースの中身に基づく。
つまり、バックエンドでしか判断ができない。
その上で、Aのフロントエンドで名前の一覧を見るパターンでは、他の画面から同じタイミングでfruitが追加されるというケースに対応ができない。
また、一般的にフロントエンドでは必須の入力欄が空などHTMLのformレベルのvalidationに留めていた方が開発も運用も楽。
最初に書いていた書き方
- fruitsのAPIから返ってきた果物名を配列にして持つ。
- その後、handleSubmitで値がその中に含まれていないかどうかをチェックする
のように書いていました。
// fruitNames = ["apple", "melon"]
const fruitNames = React.useMemo(() => {
return fruits.map(fruit => fruit.name)
}, [fruits])
// 他の処理・・・
const handleSubmit = async (newFruitName: string) => {
if (fruitNames.includes(newFruitName)) {
toast.addGeneralErrorToast()
return
}
// 他の処理・・・
}
アドバイスをもらった後の書き方
exists, err := models.Fruits(
// ORMを利用してSQLを組み立てる
).Exists(ctx, db)
if err != nil {
return // 普通のエラー
}
if exists {
return // AlreadyExistsのエラー
}
const handleSubmit = async (newFruitName: string) => {
try {
...
} catch (err: number | any) {
// AlreadyExistsをキャッチする
if (err.response.status === Entities.statusCode.CONFLICT) {
// Error toastを出す
return
}
// Success toastを出す
}
}
まとめ
バリデーションという小さい(?)分野とはいえ、何をフロントエンド側で実装し、バックエンド側で実装するかは結構難しい問題だなと思いました。
重たいバリデーションをフロントエンド側で実装しようとしている時は、疑いを持ったほうが良さそう・・・ということも知れた・・のかな。
もっともっと技術を磨きたいと思います。
あと、が果物の中で一番好き。
おわり。