バックエンドの開発を待たずにフロントエンドGraphQL開発をする
はじめに
下記のような経験がある人は多いと思います。
「新機能のフロントエンド開発をしたいけど、バックエンドの GraphQL 実装がまだ終わっていない...」
GraphQL を採用したプロジェクトでは、スキーマファーストな開発が理想とされていますが、実際の現場では「バックエンドの実装待ち」が発生することは珍しくありません。
この記事では、バックエンドの GraphQL 実装を待たずにフロントエンド開発を進める方法を、実務の際に考えて構築したので、それを共有します。
課題:開発のボトルネック
私のプロジェクトでは、Vue 3 + Apollo Client を使用しているので、その内容で紹介します。
プロジェクトでは、以下のような課題を抱えていました:
- GraphQL Codegen がリモートスキーマに依存 - スキーマの取得にバックエンド API への接続が必要
- 新規フィールド追加時の待ち時間 - バックエンドのマージを待たないと型定義が生成できない
- 並行開発の阻害 - フロントエンドとバックエンドの開発が直列化してしまう
これらを解決するため、以下の流れで修正を行いました。
解決策の全体像
┌─────────────────────────────────────────────────────────────┐
│ 開発フロー │
├─────────────────────────────────────────────────────────────┤
│ 1. ローカルスキーマ編集(バックエンド設計を先取り) │
│ ↓ │
│ 2. ローカルCodegenで型定義を生成 │
│ ↓ │
│ 3. @clientディレクティブでモックデータを使用 │
│ ↓ │
│ 4. バックエンドマージ後、リモートスキーマに切り替え │
└─────────────────────────────────────────────────────────────┘
1. schema.graphql の導入
Before: JSON 形式のスキーマ
従来はschema.json(Introspection 結果の JSON 形式)を使用していました。しかし、これには以下の問題がありました:
- 可読性が低い - JSON は人間にとって読みにくい
- 編集が困難 - 新しいフィールドを追加するのが大変
- 差分が見づらい - コードレビュー時に変更点が分かりにくい
After: SDL 形式のスキーマ
GraphQL SDL(Schema Definition Language)形式のschema.graphqlに変更しました。
# schema.graphql - 人間が読みやすい形式
type User {
id: Int!
name: String!
email: String!
# 新規フィールドもここに追加するだけ
phoneNumber: String
role: UserRole!
}
enum UserRole {
ADMIN
MANAGER
MEMBER
GUEST
}
type Query {
user(id: Int!): User
users: [User!]!
}
ローカルに存在するgraphql(SDL)ファイルを使用してのcodegen
codegen.tsで、リモートスキーマとローカルスキーマを切り替えられるようにしました:
これにより、以下のコマンドで使い分けができます:
# 通常のcodegen(リモートスキーマから取得 + codegen)
yarn codegen
# ローカルスキーマモード(すでに取得してあるスキーマをベースにcodegenを実行する)
yarn codegen:local
2. @client ディレクティブとモックデータ
ここが一番のキーポイントです。
Apollo Clientを使用していないとこちらの機能は使用できないので、もし graphql-requestなど他のgraphql clientを使用している方は独自でカスタムディレクティブなどを作成してください。
ローカルスキーマで型定義を生成しても、実際に GraphQL クエリを実行するとエラーになります。バックエンドにまだ実装されていないからです。
そこで、Apollo Client の@clientディレクティブを活用しました。
Using GraphQL directives in Apollo Client - Apollo GraphQL Docs
@client ディレクティブとは?
@clientは Apollo Client の標準機能で、「このフィールドはサーバーにリクエストせず、クライアント側で解決する」ことを示します。
query UserProfile($id: Int!) {
user(id: $id) {
id
name
email
# バックエンド未実装のフィールド
phoneNumber @client
role @client
}
}
useQuery でのモックデータ注入
@clientを付けたフィールドに値を設定するため、useQueryを拡張しました:
type Options<TResult = any> = {
/**
* Development環境でのみ動作するモックデータ
* 実際のGraphQLレスポンスにマージ
*/
mock?: DeepPartial<TResult>
}
export function useQuery<TResult, TVariables>(
query: DocumentParameter<TResult, TVariables>,
variables?: Variables,
options?: Options<TResult>
) {
// オリジナルの useQuery result (vue-apollo)
const queryResult = useQueryApolloComposable(query, variables, options)
// Development環境でmockが指定されている場合、resultにマージ
const result = computed(() => {
if (!isDev() || !options?.mock) {
return queryResult.result.value
}
// 実際のデータが存在する場合のみマージ
if (queryResult.result.value) {
return deepmerge<TResult>(
queryResult.result.value as TResult,
options.mock as Partial<TResult>
)
}
return queryResult.result.value
})
return {
...queryResult,
result,
}
}
使用例
コンポーネントでは以下のように使用します:
const { result } = useQuery(
UserProfileDocument,
{ id: userId },
{
mock: {
user: {
phoneNumber: '090-1234-5678',
role: 'ADMIN',
},
},
}
)
ポイント:
-
isDev()によるチェックで、本番環境ではモックは適用されません -
deepmergeを使用して、既存の API レスポンスとモックデータをマージ
MSWを使用することも考えましたが、このようなシンプルなやり方で済んだので、結局導入はしませんでした。
実際の開発フロー
ステップ 1: バックエンドの設計 issue を確認
まず、バックエンドチームと設計を共有し、追加される GraphQL スキーマの定義を把握します。
ステップ 2: schema.graphql をローカルで編集
# schema.graphql
# 新しい型を追加
type UserList {
id: Int!
title: String!
status: Int!
listType: Int!
createdAt: DateTime!
}
# Queryに新しいクエリを追加
type Query {
userLists(status: Int): [UserList!]!
}
ステップ 3: ローカル Codegen を実行
yarn codegen:local
これにより、型定義が生成されます!
ステップ 4: @client ディレクティブを使ってクエリを作成
query UserLists($status: Int) @client {
UserLists(status: $status) {
id
title
status
listType
createdAt
}
}
クエリ全体に@clientを付けることで、サーバーへのリクエストを完全にスキップします。
ステップ 5: コンポーネントでモックデータを使用
const { result, loading } = useQuery(
UserListsDocument,
{ status: 1 },
{
mock: {
userLists: [
{ id: 1, title: 'hoge', status: 1, listType: 1 },
{ id: 2, title: 'fuga', status: 1, listType: 2 },
],
},
}
)
ステップ 6: バックエンド実装後の移行
バックエンドにマージされたら、以下の手順で本番コードに移行します:
- 通常の Codegen を実行
- @client ディレクティブを削除
- モックオプションを削除
まとめ
この記事で紹介した方法により、以下のメリットを得られました:
| Before | After |
|---|---|
| バックエンド待ちで開発が止まる | 設計段階から並行開発が可能 |
| schema.json が読みにくい | schema.graphql で人間にやさしい |
| モックの仕組みがなく困る | useQuery に統合されたモック機能 |
| 型がないと開発が進まない | ローカル Codegen で即座に型生成 |
GraphQL のスキーマファースト開発の理想に近づきつつ、現実の開発ワークフローに対応した柔軟な仕組みを構築できました。
同じような課題を抱えているチームの参考になれば幸いです!
Discussion