🕵🏻

【GraphQL】スキーマ駆動開発におけるバリデーションの取り決め設計パターン集

2023/09/19に公開

ハコベル物流DXシステム開発部のおおいし (@bicstone) です。普段はフロントエンドエンジニアとしてハコベル配車計画の開発を行なっています。

前回の記事では、GraphQLをプロダクトに投入するにあたり検討したエラーレスポンス設計パターンについて紹介しました。

https://zenn.dev/hacobell_dev/articles/graphql-error-response

この記事では、フロントエンドとバックエンド間でのバリデーションスキーマの取り決めについて解説します。

GraphQLスキーマ設計で悩まれている方の参考になれば幸いです。

はじめに

ハコベル配車計画では、バックエンドとフロントエンド間の通信においてGraphQLを活用しています。1年ほど運用していく中で、設計における課題がいくつか表面化してきました。

今回、社内ハッカソンイベントHackWeek 2023 [1] が開催され、ハコベル配車計画チームでは1年間の運用の中で感じた、GraphQLスキーマの設計における悩みを振り返ることにしました。

バリデーションにおける課題

ハコベル配車計画では、ユーザーがフォームに入力し、GraphQLを通じて送信する画面がいくつかあります。フォームに入力された内容についてはバックエンドにおいて文字数やフォーマットなどのバリデーションを行っています。また、ユーザビリティの向上を目的にフロントエンドにおいてもリアルタイムバリデーターを実装しています。

しかし、GraphQLにおいてはバリデーションの共有に関する取り決めはないため、コミュニケーションベースでバックエンドとフロントエンドでそれぞれバリデーターを実装する必要がありました。

そのため、 1年間の運用の中で、バリデーションに関するコミュニケーションを削減するための試行錯誤を繰り返してきました。その過程で検討した3パターンのバリデーションを明示する方法と、それぞれのメリット・デメリットを紹介します。

スキーマ上でバリデーションを明示する方法

パターン 1. コメントを使う

まずは、仕様をスキーマ上にコメントで記載する方法を紹介します。

次の例では、各項目に文字数やフォーマットの情報を日本語でコメントしてスキーマ上に明記しています。

schema.graphqls
type Mutation {
  registerLocation(
    code: String! # 1文字以上, 255文字以下
    name: String! # 1文字以上, 255文字以下
    phoneNumber: String! # 日本の電話番号(ハイフンを含んでよい)
  ): Boolean!
}

メリット

  • スキーマの拡張が不要なため、スキーマがシンプルになる
  • コメントは必要に応じて詳細に書くことができたり、省略できたりする柔軟性がある

デメリット

  • バリデーターとスキーマが連携しないので実装との不一致が起こりうる
  • 型による強制が行われないので、メンテナンス性が低下する
  • ルールが定められていないため、記入者によって記載粒度が異なるなど曖昧さが生じる

評価

バリデーション仕様をコメントにて明記するため、シンプルで柔軟に書くことができるメリットはありますが、スキーマや型による制約や自動化が行えないため仕様を実装者の注意力によって担保する必要があります。

スキーマの変更について合意形成が取りやすい環境で、フロントエンド側で厳密なバリデーションがあまり必要ない場合におすすめです。

パターン 2. カスタムスカラ型を使う

次に、GraphQL の仕様に定められた機能の1つである、カスタムスカラ型を活用する方法を紹介します。

次の例では、文字数の制約とメールアドレスの制約に専用のカスタムスカラ型を作成して適用しました。

schema.graphqls
scalar Max255LengthString
scalar JapanPhoneNumber @specifiedBy(url: "https://www.soumu.go.jp/main_sosiki/joho_tsusin/top/tel_number/index.html")

type Mutation {
  registerLocation(
    code: Max255LengthString!
    name: Max255LengthString!
    phoneNumber: JapanPhoneNumber!
  ): Boolean!
}

メリット

  • スキーマの記述量が減り、よりシンプルに表記できる
  • フロントエンドでは専用の型として扱うことができる
  • 同じカスタムスカラーを使用しているフィールド間でバリデーターを共通化できる

デメリット

  • 新しいカスタムスカラ型を用いる時、議論の上でバックエンドとフロントエンドそれぞれでバリデーターを手動で実装する必要がある
  • 認識の齟齬によりバリデーション実装の不一致が起こりうる
    例えば、 JapanPhoneNumber はハイフンを含むかどうかを協議しておく必要がある
  • 似たような命名で異なるフォーマットが混同しないように命名に関して十分な設計が必要

評価

シンプルに表記でき、繰り返しにも強いメリットがありますが、カスタムスカラーの定義について事前に協議をしておく必要があります。

カスタムスカラーの意味について合意形成を取りやすい環境で、同じバリデーションを複数箇所で使用したい場合におすすめです。

パターン 3. カスタムディレクティブを使う

最後に、 graphql-ruby-constraint-directive というライブラリで提唱されているカスタムディレクティブを活用する方法です。

次の例では、各フィールドの文字数の制約を明示しています。また、電話番号においては正規表現でフォーマットを定義しています[2]

schema.graphqls
type Mutation {
  registerPoint(
    code: String! @constraint(minLength: 1, maxLength: 100)
    name: String! @constraint(minLength: 1, maxLength: 255)
    phoneNumber: String! @constraint(minLength: 9, maxLength: 12, pattern: "^0[-\d]{9,12}$")
  ): Boolean!
}

メリット

  • 送信する値の仕様がスキーマ上で明確になり、フロントエンド・バックエンド間で認識のずれが起こりにくい
  • バリデーションを自動生成できる

デメリット

  • 厳格な RFC 5322 に則ったメールアドレスなど、複雑なフォーマットを持つものに対して正規表現で表記するのは困難な場合がある
  • 各フィールドごとにディレクティブの設定が必要なため、同様のバリデーションを行うフィールドが多いような場合に繰り返しが増え、管理が煩雑になる

評価

各フィールドのバリデーションがスキーマ上で明確になり、使う側でドメイン知識が不要になるメリットがありますが、それぞれディレクティブの設定が必要なため、繰り返しが増え、管理が煩雑になるデメリットがあります。

開発規模が小さく、使う側が不特定多数でフロントエンドとバックエンド間のコミュニケーションコストを減らしたいプロジェクトでおすすめします。

事例紹介

当初、ハコベル配車計画では、 「パターン 1. コメントを書く」 を採用し、フロントエンド・バックエンド両方でバリデーターを実装していました。PoCプロジェクトであり、柔軟性を持たせたいと考えたためです。1チームで開発を進めており、スキーマの変更について合意形成が取りやすかったため、仕組み化できていなくても大きな支障はありませんでした。

ところが、1年ほど運用したところ、コミュニケーションミスでバリデーション実装差異の発覚する場面がありました。また、プロダクトが大きくなるにつれて繰り返し使用する要素が増え、バリデーターのメンテナンスコストが増加していきました。

そこで、ドメイン知識が不要で繰り返し使用する普遍的な要素のみを 「パターン 2. カスタムスカラ型を使う」 へ置き換えることにしました。 ( TimePrefectureIDURI など)

この変更により、バリデータの共通化のメリットを活かすことができました。その他のフィールドについては 「パターン 1. コメントを書く」 のままとし、フロントエンドでのバリデーションを最小限度としました。必要な場合はコメントで補足しています。

背景として、To B向けのサービスであることからバリデーションエラーがほとんど発生していませんでした。そのためフロントエンドでのバリデーションは、コメントでバリデーション条件が明記された場合とカスタムスカラを指定された場合を除き、必須チェックと型チェックのみに留めています。

一方で、「パターン 3. カスタムディレクティブを使う」 の導入も検討しましたが断念しました。スキーマにドメイン知識が露出してしまうことや、複雑なバリデーションを実装することで不具合やメンテナンスコストが高くなってしまうデメリットが大きいと判断したためです。

まとめ

GraphQL開発におけるバリデーションの取り決めについて検討した3つのパターンについて紹介しました。

私達は 「パターン 1. コメントを書く」「パターン 2. カスタムスカラ型を使う」 の組合せを選びましたが、それぞれメリット・デメリットがあるため、プロジェクトの特性によって適宜選択することが重要です。

GraphQLはスキーマ駆動開発に最適だとよく言われていますが、ユーザーが想定外の入力をあまりしない場合などプロジェクト特性によっては、フロントエンドバリデーションが不要となり、バックエンドのバリデーションのみで十分な場合もあります。どこまでスキーマで表現するかの判断は難しいので、同じように設計で悩んでいる方のヒントになれば幸いです。

前回の記事では、GraphQLをプロダクトに投入するにあたり検討したエラーレスポンス設計パターンについて紹介しているので、ぜひお読みください。

https://zenn.dev/hacobell_dev/articles/graphql-error-response

次回は、チームメンバーの @H0R15H0 さんからGraphQLのカスタムスカラーの使用パターン集を公開する予定です。

参考文献

脚注
  1. 「HackWeek2023 」イベントレポート - RAKSUL TechBlog ↩︎

  2. 実用性を重視し、最低限度の正規表現としています。 ↩︎

Hacobell Developers Blog

Discussion