21
16
お題は不問!Qiita Engineer Festa 2024で記事投稿!
Qiita Engineer Festa20242024年7月17日まで開催中!

【初学者はとりあえずこれでOK】とにかく書いてみるGraphQL スキーマ設計3ステップ

Last updated at Posted at 2024-07-02

はじめに

この記事が、GraphQL はじめてみたいけど、スキーマ設計とかクライアントライブラリとか、色々な情報が溢れていてわからないといった初学者向けに、まずはこれで書いてみよう!と迷わず実際に触るための一助になれば嬉しいです。

スキーマファーストとコードファースト

GraphQLを使ったAPIの開発では、2つの進め方があります。
GraphQLってなにー??な状態の方はまずこちらの記事の前半部分をどうぞ。

進め方1つ目のスキーマファーストでは、まずGraphQLスキーマ(型定義)をサーバーチーム、フロントチームが一緒に話して作成します。作成したスキーマの設計を基盤として、両チームが実装を並行して進めていくことができます。

2つ目のコードファーストは、GraphQLスキーマ(型定義)を、主にサーバーチームが作成します。まずサーバーのコードを書き、そこから自動的にスキーマが生成されます。

NestJSを例にすると、デコレーター(@Resolver@Queryなど)を使ってリゾルバ関数を定義し、TypeScriptの型情報からスキーマを生成します。フロントチームはこのスキーマを元に実装を進めていきます。(参考:GraphQL + TypeScript | NestJS)

この記事では、1つ目のスキーマファーストに初めて取り組む方を対象に、GraphQLスキーマを書く基本的なステップをまとめます。

スキーマファイルの作成は、バックエンドとフロントエンドで一緒に進める協働作業です。
スキーマ設計は一緒に議論しながら進められる環境がベストです。

ステップ1:扱うリソースの定義

今回はTODOアプリを作る例で進めます。

扱うリソースは、 Todo, Userと考えて、スキーマ定義は下記のようにします。

schema.graphql
type User {
	id: ID!
	name: String!
}

type Todo {
  id: ID!
  description: String
  ExpireDate: Timestamp
  user: User ←ココでUserリソースのグラフ構造を定義
}

ここでのポイントは、id参照している箇所(リレーションのFKを持っているリソースなど)は、idだけではなく、参照先のリソースをグラフ構造で持つことです。

このようにすることで、実際にQueryを使用するとき、グラフ構造を使って1度のリクエストで、複数のリソースを一気に取得することができます。

クライアント側の例

dataProvider.ts
import gql from "graphql-tag"

gql`
    flagment todoFields on Todo {
        id
        description
    }
;`

const query = gql`
    query {
      findTodoByUserId(id: 1) {
        ...todoFields
        user {
          name
        }
      }
    }
;`

ステップ2:GraphQL APIの定義 Query編

では、先ほど上げたQueryを例にAPIの定義編に入ります。
今回は特定のユーザーに紐づくTODOを全て取得するAPIを定義してみます。

schema.graphql
type Query {
	findTodoByUserId(id: ID!): [Todo]!
}

今回は全件取得なので複数のデータが返る想定で、Todoの配列を返す定義にしています。

もしTodoが必ず1つは存在するようなデータなら、[Todo!]!として配列と、その中身も必須の型にするとよいでしょう。

一方で、今回のような空データもあり得る場合に、[Todo] としてしまうと、レスポンス結果がnullであることを許容してしまいます。フロント側からすると、返る可能性のある型がnull, [], [Todo, Todo]となってしまい、不便である場合が多いです。

そのため、まずは0件があり得る複数データの場合、必ず空配列が返る[Todo]!としておくと良いと思います。

ステップ3:GraphQL APIの定義 Mutation編

スキーマ定義の最後のステップです。
TODOを作成するMutationを定義します。

schema.graphql
type Mutation {
    createTodo(input: CreateTodoInput!): CreateTodoPayload!
}

input CreateTodoInput {
    user_id: ID!
    description: String!
}

type CreateTodoPayload {
    todo: Todo
    errors: [CrateTodoError]!
}

type CreateTodoError {
    message: String!
}

上記の例で特に始めのうち、守るべきところはMutationの返り値に専用の型を用意しているところです。
このようにしておけば、返したい値の追加も簡単に行うことができます。
また、先程の例ではQueryの返り値に直接リソースの型を定義していましたが、もしtotalなど別の値も返したい場合は、Payloadを定義すると良いでしょう。

業務エラーはGraphQLレスポンスのerrorsではなく、Payloadに定義したerrorsで返すようにしましょう!
詳しい理由が知りたい方は下記エラーハンドリングの記事と、ステップ5で紹介している記事に目を通してみてください。

Payloadのerrorsキーの型CreateTodoErrorはベースのinterface型を定義しておいて、それだけを使うでもいいし、個別のtypeに継承させてより厳密に定めてもいいです。

GraphQLのエラーハンドリングに関する参考記事
GraphQLにおけるエラーハンドリングの選択肢と検討
GraphQLにおけるエラーハンドリングの仕方 - ZOZO TECH BLOG

クライアント側の例

dataProvider.ts
const mutation = gql`
    mutation {
      createTodo(input: CreateTodoInput!) {
          todo {
            ...todoFields
          }
          errors {
              ... on CrateTodoError {
                  message
              }
          }
       }
    }
;`

これで一通りのGraphQLスキーマが完成しました!
スキーマファイルさえ完成してしまえば、サーバーサイドとフロントサイドの関心事はスキーマファイルを起点に分離されるため、お互いが後は煮るなり焼くなり自由に開発を進めることができます。


ですが、本当にGraphQLを活かすためにはまだもう少しステップもあります。
今回はとっつきやすい参考記事のみ紹介できればと思います。

ステップ3.5:その他APIの定義

複雑な業務ロジック、エラーハンドリング、キャッシュ制御など、初めてのGraphQLスキーマ設計には様々な困難が待ち受けています。好き放題やれば簡単に破綻します。。。
しかし常に、設計原則に立ち返りつつ定義していけばきっと大丈夫です。

ステップ4:フロントサイドのGraphQLクライアントを検討しよう

GraphQLは、ただFetchAPIでリクエストすることもできます。
しかし、一般的には何かしらのクライアントライブラリを用いることが多いと思います。
graphql-requestやApolloClientが有名ですね。

まずはgraphql-requestを使って、ステップ5のコードの自動生成ができるところまで検討&実際に触ってみることがいいと思います。
少しイメージが湧いてきたら、下記のような記事で、他の選択肢を検討してみましょう。

ちなみに、、、今回提案した、とにかく書いてみるスキーマは、Apollo Clientのキャッシュ機能に最低限準拠した書き方ですので、ApolloClientを採用する場合は、CachePolicyだけ知っておけば早速キャッシュを体験することができます。

ステップ5:GraphQLスキーマを元に型を生成

GoやTypeScriptなど、静的型付け言語を用いる場合、サーバーサイド、フロントサイド共に、やり取りする型をスキーマから自動生成することができます。

型の自動生成は、スキーマファーストで開発を行うなら必ずやりたいです。
また、型情報だけでなく、リゾルバのコード生成やORMとの連携など、スキーマを中心にした自動生成はぜひ検討してみましょう。

さいごに

ここまで来れば、あなたも立派なGraphQLユーザーです!
GraphQLは関心事の分離のみならず、自動生成やキャッシュ、Fragment Colocationなど、様々なメリットが存在していますので、フロントエンドエンジニア、バックエンドエンジニアそれぞれの領域でぜひ活用してみてください。

付録

今回書き上げたスキーマファイルは最終的にこのような状態です。

schema.graphql
type Query {
	findTodoByUserId(id: ID!): [Todo]!
}

type Mutation {
    createTodo(input: CreateTodoInput!): CreateTodoPayload!
}

type User {
	id: ID!
	name: String!
}

type Todo {
  id: ID!
  description: String
  ExpireDate: Timestamp
  user: User
}

input CreateTodoInput {
    user_id: ID!
    description: String!
}

type CreateTodoPayload {
    todo: Todo
    errors: [CrateTodoError]!
}

type CreateTodoError {
    message: String!
}
21
16
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
21
16