avatar
screenshot
2023年10月〜

evodo/graphql

GraphQLを使用したTodoリストです。

概要

GraphQLを試すために作ったTodoリストです。 型安全なAPIリクエストを実現する技術としてのGraphQLを試してみたくて作りました。

Googleアカウントでログインすることができ、初回ログインではユーザー登録を行います。 クライアントサイドでのバリデーションも実装しています。

ログインページ ユーザー登録ページ

タスクの詳細はサイドパネルに表示されるようになっています。 タスクの説明を入力できたり、タスクにコメントを追加していくことができます。

タスク詳細ページ

使用した技術

  • TypeScript
  • GraphQL Yoga
  • Apollo Client
  • Prisma
  • Jest
  • Next.js
  • Radix UI
  • Tailwind CSS
  • Firebase Authentication

GraphQLのバックエンド

バックエンドではGraphQL Codegenのgcg-typescript-resolver-files を使用しています。 これを使用することで、GraphQLのスキーマを書くと、自動でresolverの雛形や型定義ファイルを生成してくれるのでとても便利です。 特にGraphQLリクエストとデータソース間のデータ変換を行うMapperを自動で設定してくれる機能が便利で、 schemaName.mapper.tsのファイルを置いておくと、自動でMapperが適用された型定義が生成されます。 これを使わないと、GraphQL Codegenの設定ファイルに文字列でMapperのパスを指定する必要があるので面倒です。

他には、バックエンド側でgraphql-constraint-directiveを使用して、 ディレクティブを用いて簡単なバリデーションを行えるようにしています。 そのディレクティブを含んだスキーマから、フロントエンド側でtypescript-validation-schema を使用してzodでvalidation schemaを生成しています。 このスキーマをreact-hook-formのresolverに渡すことで、クライアントサイドでのバリデーションを実装しています。

また、バックエンドとフロントエンドをpnpmのワークスペースを使ってモノレポにしているのですが、 GraphQL Configを使用して、両方のConfigファイルを一つにまとめて管理しています。

ほとんどすべてのMutationやQueryのテストを書いています。 テストを実行するためのライブラリなども充実していて便利でした。

GraphQLのフロントエンド

ReactでのGraphQLクライアントはいくつか種類があります。 最初はurqlを使用していたのですが、楽観的UIを実装するためにキャッシュを書き換える必要があるのですが、 urqlではキャッシュを一箇所で管理する必要があります。 Mutationを行う場所でキャッシュを更新することができず、別の場所でMutation名をkeyとした関数によってキャッシュを更新することになります。 自動で型は付かないので、GraphQL Codegenが生成した方ファイルを使って型をつけようかと思ったのですが、難しくて断念しました。

ほかにも、認証付きでリクエストを送ることはできるのですが、認証情報が変わったときに毎回urqlクライアントを再生成する必要があったりと、 シンプルな実装なら問題ないのですが、少し複雑なことをやろうと思うと面倒くさくなるという印象を受けました。

そのあとにApollo Clientを試しました。 普段はAPIリクエスト周りでtanstack-queryというライブラリを使っているのですが、それがApollo Clientと似ていたので使いやすかったです。

ただ、よくわからない挙動があり、解決にとても時間がかかってしまいました。 このアプリでは、タスクのメモを取得する際に、カーソルページネーションを実装しています。 その状況でメモの削除を楽観的更新で行おうとして、キャッシュを手動で書き換えても何故か再レンダリングされないという問題です。 デバッグしていると、キャッシュは更新されているということがわかったので、なんとかして再レンダリングさせようとしたのですが、 当時は全く情報を探すことができませんでした。 そのコードを書くならこのコールバックの中だよなあと考え、引数として渡されているオブジェクトの中にそれっぽいメソッドが無いかを適当に試していたら解決しました。 そのメソッドのドキュメントを探したのですがどこにも存在せず、多分内部的に使用される関数だったと思うのですが、 メソッド名だけを頼りに問題を解決したことがなかったのでとても興奮していました・・・。

認証周り

認証はFirebase Authenticationを使用しています。 ユーザー情報はバックエンドのDBに入っているので、紐づけるような実装を行っています。 また、初めてログインしたユーザーは新規登録ページに遷移させて、ユーザーの基本的な情報を入力してもらう実装になっています。 フロントエンド側でFirebase AuthenticationのIdトークンを持っていても新規登録を行っていないことが考えられるので、 それだけでログイン済みであるとは判断できません。 そのため、Firebaseでログインしているかの状態と、バックエンドにあるユーザーの状態をあわせてログイン情報としています。 各状態のローディング状態やエラー状態などを考慮しながらログイン情報を作る必要があるので、そのあたりがすこし複雑でした。 もう少しシンプルに実装できるかもしれません。

新規登録の流れは、

  1. Firebase Authでログインさせたあとに、新規登録でなければfalseを返して、 新規登録で、仮ユーザーが作成されていない場合には仮ユーザーを作成してtrueを返すようなAPIを実行する
  2. 新規登録であれば新規登録ページにリダイレクトさせる
  3. 新規登録ページでバックエンドから仮ユーザーの情報を取得して、フォームの初期値にする
  4. 新規登録リクエストを送り、仮ユーザーを削除してユーザーを作成する

1でいちいち仮ユーザーを作っているのは、特定のユーザーが新規登録中かを判定するためです。 実際にバックエンドで、特定のユーザーが新規登録中かの状態が必要になるケースは思いつかないのですが、実装を試してみたかったので作りました。

UI

今回はスタイルのついているUIコンポーネントライブラリを使用せずに、 ヘッドレスUIライブラリであるRadix UIを使用して、スタイルはTailwind CSSを使って自分で当てています。 以前も使用したことはあったのですが、本格的に使い始めたのはこのあたりだったと思います。

GraphQLを試してみて

このプロジェクトは、型安全なAPIリクエストを実現する手段としてのGraphQLを試すためのプロジェクトです。 バックエンド側では、スキーマを書くだけでリゾルバの雛形が自動で生成されるため、とても体験が良かったです。 フロントエンド側ではクエリに型が効き、ある程度は快適に実装することができました。 ただVSCodeのGraphQLのLSPで、ただファイルを保存するだけではGraphQLの変更が反映されなかったため、 更新するコマンドにショートカットを割り当てて実行していました。

エコシステムとしては、一つのものでいろいろなことができるようになるわけではなく、 必要な細かいライブラリをいくつも使用する必要がありました。 関連するライブラリがそこそこ多いので、調べるのが少し大変でした。

GraphQLは、どのエンドポイントにどの情報をもたせるのかを考える必要がないため、APIを実装するのは楽でした。 フィールドに対してどのようにデータを取得してくるかだけを書けば良かったです。 N+1問題についても、今回使用したPrismaというORMには、 tick単位でバッチ処理で取得してくれるAPI があるので特に問題はなかったと思います。