GraphQL スキーマ駆動開発の極意【スキーマ定義を最大限効率化する方法】
はじめに
前談
サーバーサイドエンジニアのさばくんとふぐさんは GraphQL を用いた大規模プロジェクトに参加することになりました、が、、、
🐟 「ふぐ先輩! あんまり開発期間もないですし、できるだけ効率的に進めたいですね」
🐡 「おし、それなら、 GraphQL をスキーマ駆動にして、フロントエンドとバックエンドを非同期で開発するぜ〜〜」
🐟「はい!じゃあスキーマとにかく書き殴ります!!👊」
~~ 3ヶ月後 ~~
🐟 「ふぐ先輩!!! スキーマがヨメませーーーん! 書けませーーーん!! 誰も参照していませーーーん!!!」
🐡 「ぐふ...」
本題
GraphQL はスキーマがあることを前提とした API 仕様であり、特にスキーマ駆動開発においてはスキーマをどう定義するかというのは何より大事なことです。そして、良いスキーマを定義するためには、 スキーマ定義の DX (開発者体験)を高めることも同じく重要です。
今回は、スキーマ定義、ひいてはスキーマ駆動開発を効率化するためのエコシステムと自分なりのプラクティスを紹介したいと思います!
この記事の内容は以下となります٩( ᐛ )و
⭕️ スキーマ定義を効率化する
⭕️ スキーマ定義と実装をすり合わせる
⭕️ CI/CD でスキーマの検査と共有を行う
⭕️ 👆のための、ツール群の紹介
❌ コードファースト vs スキーマファーストの話
❌ スキーマの定義の仕方
❌ クライアントコード自動生成の方法
❌ 特定の FW での GraphQL 実装
他の内容については関連記事などにまとめていますのでご覧ください 👍
スキーマ定義を効率化する
スキーマを検査する
まずは、Lint ツールの紹介です。スキーマを効率よく定義するためには、開発中常にスキーマが正しく、最適な状態を保つ必要があります。
このツールでは、例えば以下のようなルールを適用することができます。- 間違った型が使われていないこと
- 説明が type や field についていること
- 定義した型が一回以上使われていること
- field が camelCase であること
また、必要に応じて設定でルールを ON/OFF することができるので、その点も良いです。
エラーの出力例
スキーマを分割する
GraphQL のスキーマは全体で1つの.graphql
ファイルに集約されるのですが、大規模になってくると可読性がどうしても下がってしまいます。
そういった時は、👆のツールを用いることで、いくつかのモジュールにスキーマを分割して定義することができます。
merge-schema.js // 1. 実行!
generated.graphql // 2. 生成!
source
├── book.graphql
├── common.graphql
├── scalar.graphql
└── user.graphql
pre-commit でマージと検査を自動化する
上記2つを追加したら、 githook の pre-commit でコミット前に正しく定義できたか検査すると良いでしょう。手順としては、
.graphql
のファイルの変更があったら
- スキーマをマージする
- 生成されたスキーマを Lint する
- 各スキーマをフォーマットする( prettier など)
pre-commit を shell script で実行する場合は例えばこのようになります。
#!/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 を気に入っています。
ただし、 async-graphql はコードからスキーマ生成をすることはできるものの、スキーマからのコード生成に対応していなかったため、スキーマと実装をどうにかすり合わせる必要がありました。
(👇 のように有志の方が対応しているものはあります。)
スキーマ間の Diff をとる
自分たちが記述したスキーマとサーバーから生成したスキーマの差分をとることで、常に自作のスキーマを正とした状態を保つようにします。
そのためには、このツールがおすすめです。
自作のスキーマを更新した場合、例えば以下のようなメッセージが表示されます。
$ 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 の例
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 であることを条件とした時に一番見やすいと思ったツールがこちらでした。
実際には以下のような 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
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
スキーマ駆動開発について知りたい方はこちら
クライアントコードの生成例( Next.js )について知りたい方はこちら
その他便利ツールについて
Discussion