JavaScriptでバリデーションするときのAPI設計

フォームのバリデーションをJavaScriptでやりたい場合、どんなAPI設計にするのが良いか?

ユーザーのバリデーションが下記のように定義されているとする。

class User < ApplicationRecord
  validates :user_name, presence: true, uniqueness: true, format: { with: /\w/ }
  validates :age, numericality: { only_integer: true, greater_than_or_equal_to: 0 }
end

このバリデーションをどうにかJavaScript側からも使いたいけど、こういう場合はバリデーション用のAPIを毎回作る感じですかね?

module Validations
  class UsersController < ::ApplicationController
    # POST /validations/user.json
    def create
      if @user.valid?
        render json: []
      else
        render json: @user.errors
      end
    end
  end
end

もしくは、モデル単位じゃなくて各フォームでAPIを分けておいた方が良かったりするかも?

module Validations
  class SignupFormsController < ::ApplicationController
    # POST /validations/signup.json
    def create
      # do something
    end
  end
end

あとは通信を極力減らすため、あえてJavaScriptで実現可能なバリデーションはJavaScript側に冗長でも実装してしまい、DBのアクセスが必要なものだけAPIを用意する方法もある?

uniquenessだけはDBをチェックする必要があるので、それだけAPIにする例。

class UserNamesController < ApplicationController
  # GET /user_names/:user_name
  def show
    if User.where(user_name: params[:user_name).exists?
      head :ok
    else
      head :not_found
    end
  end
end

本題から逸れるけど「200 OK は存在するユーザー名なので登録できないようにエラーメッセージを表示する」のは分かり辛いと思いつつ、RESTful に作るとAPIはこうなるのでは…という複雑な気持ち。

以前同じような悩みを抱えたことがあります。

その際は結局HTML5で実現可能な範囲のバリデーションを書き、他はJavaScriptというかフロントエンド側で処理することは諦めました。API設計に対する回答としては自分でもどうかと思いますが妥協案として…

どこまでやるかにも依るのですが、transaction resource を使うという方法はどうでしょうか。

POST /user_transactions, PATCH /user_transactions/:id でこういう response を返すイメージです。

{
  "id": 12345,
  "errors": [
    {
      "type": "https://example.net/validation-error",
      "title": "バリデーションエラーが発生しました",
      "invalid-params": [
        {
          "name": "user[name]",
          "reason": "このユーザー名は既に使用されています"
        }
      ]
   }
}

(あるいはちゃんと RFC7807 に沿って POST 時に application/problem+json を返してあげても良さそう)
transaction はまた別の難しさが発生したり、場合によってはオーバースペックかと思いますが、バリデーションだけに着目するとフロント側でサーバを慮ったバリデーションロジックを書かなくて済む、というメリットがありそうです。

「いいね!」 1

プロジェクトの要件によって変わってきそうですが、基本的には

が良いのかな、と思っています。

※サーバ負荷に余裕があって手抜きが必要(クライアントサイド側を作り込む余力がない)みたいな場合を除く

HTTPのステータスコードと表示を合わせたいのであれば、こういう感じでもいけませんかね

class UserNamesValidationsController < ApplicationController
  # POST /user_names/:user_name/validation
  def create
    if User.where(user_name: params[:user_name).exists?
      head ::unprocessable_entity
    else
      head :ok
    end
  end
end