🐚

GraphQL スキーマ駆動開発の極意【スキーマ定義を最大限効率化する方法】

に公開

はじめに

前談

サーバーサイドエンジニアのさばくんとふぐさんは GraphQL を用いた大規模プロジェクトに参加することになりました、が、、、

🐟 「ふぐ先輩! あんまり開発期間もないですし、できるだけ効率的に進めたいですね」
🐡 「おし、それなら、 GraphQL をスキーマ駆動にして、フロントエンドとバックエンドを非同期で開発するぜ〜〜」
🐟「はい!じゃあスキーマとにかく書き殴ります!!👊」

~~ 3ヶ月後 ~~

🐟 「ふぐ先輩!!! スキーマがヨメませーーーん! 書けませーーーん!! 誰も参照していませーーーん!!!」
🐡 「ぐふ...

本題

GraphQL はスキーマがあることを前提とした API 仕様であり、特にスキーマ駆動開発においてはスキーマをどう定義するかというのは何より大事なことです。そして、良いスキーマを定義するためには、 スキーマ定義の DX (開発者体験)を高めることも同じく重要です。
今回は、スキーマ定義、ひいてはスキーマ駆動開発を効率化するためのエコシステムと自分なりのプラクティスを紹介したいと思います!

この記事の内容は以下となります٩( ᐛ )و
⭕️ スキーマ定義を効率化する
⭕️ スキーマ定義と実装をすり合わせる
⭕️ CI/CD でスキーマの検査と共有を行う
⭕️ 👆のための、ツール群の紹介
❌ コードファースト vs スキーマファーストの話
❌ スキーマの定義の仕方
❌ クライアントコード自動生成の方法
❌ 特定の FW での GraphQL 実装

他の内容については関連記事などにまとめていますのでご覧ください 👍

スキーマ定義を効率化する

スキーマを検査する

まずは、Lint ツールの紹介です。スキーマを効率よく定義するためには、開発中常にスキーマが正しく、最適な状態を保つ必要があります。
https://github.com/cjoudrey/graphql-schema-linter
このツールでは、例えば以下のようなルールを適用することができます。

  • 間違った型が使われていないこと
  • 説明が type や field についていること
  • 定義した型が一回以上使われていること
  • field が camelCase であること

また、必要に応じて設定でルールを ON/OFF することができるので、その点も良いです。


エラーの出力例

スキーマを分割する

GraphQL のスキーマは全体で1つの.graphqlファイルに集約されるのですが、大規模になってくると可読性がどうしても下がってしまいます。
https://the-guild.dev/graphql/tools/docs/schema-merging
そういった時は、👆のツールを用いることで、いくつかのモジュールにスキーマを分割して定義することができます。

merge-schema.js // 1. 実行!
generated.graphql // 2. 生成!
source
├── book.graphql
├── common.graphql
├── scalar.graphql
└── user.graphql

pre-commit でマージと検査を自動化する

上記2つを追加したら、 githook の pre-commit でコミット前に正しく定義できたか検査すると良いでしょう。手順としては、

.graphqlのファイルの変更があったら

  1. スキーマをマージする
  2. 生成されたスキーマを Lint する
  3. 各スキーマをフォーマットする( prettier など)

pre-commit を shell script で実行する場合は例えばこのようになります。

pre-commit
#!/usr/bin/env sh

staged_gql=$(git diff --name-status --cached | grep -E '^[^D]\s*.*\.graphql$' | sed -e  's/^R[0-9]\{3\}\t//' | cut -f2-)
if [ "$staged_gql" != "" ]; then
    npm run merge
    npm run lint generated.graphql
    npm run format $staged_gql
fi

スキーマ定義と実装をすり合わせる

前置き

個人的におすすめの GraphQL フレームワークは Rust の async-graphql です。もともとは @nestjs/graphql を使っていたのですが、言語の型の強さが GraphQL と相性がよく安心感があること、 Field Resolver の仕組みが GraphQL の構造と一致していることなどから今は asnc-graphql を気に入っています。
https://github.com/async-graphql/async-graphql

ただし、 async-graphql はコードからスキーマ生成をすることはできるものの、スキーマからのコード生成に対応していなかったため、スキーマと実装をどうにかすり合わせる必要がありました。
(👇 のように有志の方が対応しているものはあります。)
https://zenn.dev/tak_iwamoto/articles/43dfe5f624b154

スキーマ間の Diff をとる

自分たちが記述したスキーマとサーバーから生成したスキーマの差分をとることで、常に自作のスキーマを正とした状態を保つようにします。
そのためには、このツールがおすすめです。

https://the-guild.dev/graphql/inspector

自作のスキーマを更新した場合、例えば以下のようなメッセージが表示されます。

$ graphql-inspector diff 自作のスキーマ サーバー生成のスキーマ 

Detected following changes:
 
 Field posts was removed from object type Query
 Field modifiedAt was removed from object type Post
 Field Post.id changed typed from ID to ID!
 Deprecation reason "No more used" on field Post.title was added
 
ERROR Detected 2 breaking changes!

このように幾つかのレベル(非破壊的変更、危険な変化、重大な変更)での結果が出るので、これを見ながら、サーバー側の実装を修正し、この結果を減らすという方法でスキーマに実装をすり合わせることができます。

CI/CD でスキーマの検査と共有を行う

検査 CI/CD

前の章でおこなった検査を諸々 CI にも組み込みます。こうすることで、ローカルで誤って検査をスキップしてしまった場合などでも検査が行われます。

GitHub Actions の例
schema.yaml
name: "GraphQL Schema"

on:
  push:
    branches:
      - main
    paths:
      - "*.graphql"
      - ".github/workflows/schema.yaml"
  pull_request:
    branches:
      - main
    paths:
      - "*.graphql"
      - ".github/workflows/schema.yaml"
  workflow_dispatch:

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - name: setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: 18.12.1
          cache: "npm"

      - name: install dependencies
        run: npm ci

      - name: merge
        run: npm run merge

      - name: lint api
        run: npm run lint generated.graphql

スキーマを View で共有する

実は本記事で一番伝えたいのはこのプラクティスです。
REST でいう Swagger UI のように、GraphQL のスキーマを View に変換して見やすい形で共有するツールがあります。
そのようなツールは探すといくつかあるのですが、サーバーコストを考えて、静的 HTML であることを条件とした時に一番見やすいと思ったツールがこちらでした。

https://github.com/2fd/graphdoc

実際には以下のような View でスキーマを表示することができます。検索などもできるのがとても良いです。


https://2fd.github.io/graphdoc/github/

スキーマが更新されるたびに CI/CD を回してこの HTML をデプロイすることで、常に見やすい形で最新のスキーマを共有し続けることが可能です。また、日付でデプロイ URL を分けることで、バージョン管理をすることもできます。

GitHub Actions => S3 にデプロイする例

事前の準備として、OIDC Provider や S3 の準備が済んでいるものとします。
参考: https://zenn.dev/kou_pg_0131/articles/gh-actions-oidc-aws

schema.yaml
name: "GraphQL Schema"

on:
  ...(同上)

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      ...(同上)
      
      - name: save schema
        uses: actions/upload-artifact@v3
        with:
          name: graphql-schema
          path: generated.graphql

  build-doc:
    if: ${{ github.event_name == 'push' || github.event_name == 'workflow_dispatch' }}
    needs: test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - name: download schema
        uses: actions/download-artifact@v3
        with:
          name: graphql-schema
	  
      - name: setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: 18.12.1
          cache: "npm"

      - name: install dependencies
        run: npm ci

      - name: generate gqldoc
        run: npx graphqldoc -s ./generated.graphql -o ./doc

      - name: save schema-doc
        uses: actions/upload-artifact@v3
        with:
          name: graphql-doc
          path: ./doc

  deploy:
    if: ${{ github.event_name == 'push' || github.event_name == 'workflow_dispatch' }}
    needs: build-doc
    runs-on: ubuntu-latest
    permissions:
      id-token: write
      contents: read
    environment:
      name: graphql-schema
      url: ${{ steps.graphql-schema-deploy.outputs.deploy_url }}
    steps:
      - uses: actions/checkout@v3

      - name: Set current datetime as env variable
        env:
          TZ: "Asia/Tokyo"
        run: echo "CURRENT_DATETIME=$(date +'%Y-%m-%d-%H-%M-%S')" >> "$GITHUB_ENV"
	
      - name: Download graphql doc
        uses: actions/download-artifact@v3
        with:
          name: graphql-doc
          path: ./doc

      - name: aws login
        uses: aws-actions/configure-aws-credentials@v1
        with:
          aws-region: ap-northeast-1
          role-to-assume: arn:aws:iam::$ACCOUNT_ID:role/$ROLE_NAME # 要変更

      - id: schema-deploy
        name: s3 push
        run: | # 要変更
          aws s3 sync ./doc s3://$BUCKET_NAME/${{ env.CURRENT_DATETIME }} 
          echo "deploy_url=http://$DEPLOY_URL/${{ env.CURRENT_DATETIME }}" >> "$GITHUB_OUTPUT"

終わりに

その他にも「こういう方法どう??」って話などぜひコメントください🐟

関連記事など

スキーマ定義の仕方を知りたい方はこちら

Shopify/graphql-design-tutorial

スキーマ駆動開発について知りたい方はこちら

https://zenn.dev/manabu/articles/35ea2ddfe2df3a

クライアントコードの生成例( Next.js )について知りたい方はこちら

https://zenn.dev/yumemi_inc/articles/3258404f2b41d0

その他便利ツールについて

https://hygraph.com/blog/graphql-tools

ゆめみ

Discussion