GraphQL についてのスクラップブック

GraphQL についてのスクラップブック

February 14, 2023

プロダクトへの採用を検討しつつ今回初めて GraphQL に触りました。最終的には採用を見送ったものの、今回のメモをスクラップ形式で残しておきます。

なおサーバ/クライアントともに TypeScript 上で使用することを想定しています。

GraphQL のドキュメント類 #

公式およびそれに準ずるような情報を見たければ GraphQL.org と Apollo のサイトが良さそう。

スキーマの書き方 #

スキーマ定義の書き方は何通りかある。

パターン1:単なる文字列 #

js/ts ファイル上に単なる文字列としてスキーマを書く。(+エディタの言語識別を効かせるために #graphql のコメントをつける)

schema.ts

const typeDefs = `#graphql
  type Book {
    title: String
    author: String
  }

  type Query {
    books: [Book]
  }
`;

こういう書き方も見かける。

const typeDefs = /* GraphQL */ `
  type Book {
    title: String
    author: String
  }

  type Query {
    books: [Book]
  }
`;

パターン2:gql 関数を通す #

graphql-tag の gql 関数に文字列を渡すと GraphQL AST に変換してくれる。

schema.ts

import gql from 'graphql-tag';

const typeDefs = gql`
  type Book {
    title: String
    author: String
  }

  type Query {
    books: [Book]
  }
`;

キャッシュの機能もある。

This package only has one feature - it caches previous parse results in a simple dictionary. This means that if you call the tag on the same query multiple times, it doesn’t waste time parsing it again

合わせてここら辺の話も頭に入れておくべき。

パターン3:.graphql (.gql)ファイル #

スキーマファースト開発の形をとるならば、サーバ側でスキーマを定義する場合には、こちらが一番正式な形式だと思われる。なによりまず(言語によらない).graphql ファイルがあり、それに対して実装言語でコードを書いていく、という流れが本来あるべきもののはずなので。

schema.graphql / schema.gql(拡張子はどちらも可)

type Book {
  title: String
  author: String
}

type Query {
  books: [Book]
}

これまでのような js/ts ファイル上に記述する形ならば、他の js/ts ファイルから import することも容易だが .graphql ファイルとして定義する場合はそのままでは import できない。

そのため実装側のコード上に取り込むためには、Nodejs の readFileSync であったり @graphql-tools/load-filesloadFilesSync などを使う。

import { readFileSync } from 'fs';

const typeDefs = readFileSync('./schema.graphql', { encoding: 'utf-8' });
import path from 'path';
import { loadFilesSync } from '@graphql-tools/load-files';

const typesArray = loadFilesSync(path.join(__dirname, './types'));

なお、ES module で書いている場合 __dirname はそのままだと使えないため、次のようにする。

Void はない #

GraphQL のスカラー型は5つしかない。String、Int、Float、Boolean、ID で以上。TypeScript にすると、String と ID が string 型で、Int と Float が number 型で、Boolean が boolean 型。

例えば Mutation で何も返すものがないときにも Void は使えないため、形だけでも何かしら返すように定義する必要がある。

それっぽくやりたいなら、Void を意味するカスタムスカラーを定義すればよく、GraphQL Scalars のパッケージには Void 型が用意されているのでそれを使えば良い。

引数がある・ない場合の Query/Mutation の書き方 #

type Query {
  books: [Book!]! # 引数がない場合はこう書く
  book(bookId: ID!): Book # 引数がある場合はこう書く
}

type Mutation {
  sayHello: String # 引数がない場合はこう書く
  setMessage(message: String!): String # 引数がある場合はこう書く

  # setMessage(): String # 👈 引数がない場合にこう書きがちだけどこれはエラー
}

スキーマを分割・マージする #

typeDefs の定義ファイルを分割・マージするには @graphql-tools/mergemergeTypeDefs を使う。

TS ファイル上に gql 関数なりで定義しているのであればそれを export/import すれば良い。.graphql ファイルで定義しているのであれば loadFilesSync などで読み込む。

1つにまとめた結果をログやファイルに出力することもできる。

やろうと思えばこのように色々なところから引っ張ってきてまとめることもできる。

一方で Resolver のほうは純粋な JavaScript コードなので通常通り export/import すれば OK.

一応 @graphql-tools/mergemergeResolvers を用意してくれているが、使うこともないと思われる。

Beware that mergeResolvers is simply merging plain JavaScript objects.

スキーマから TypeScript の型を生成する #

GraphQL Code Generator を使って、スキーマから TypeScript の型ファイルをコード生成する。

アプリ全体を見渡してどんな感じになるのか、こちらの記事が分かりやすい。

N + 1 対応 - DataLoader #

GraphQL が DataLoader というライブラリを用意してくれていて、これを使えば N + 1 対策ができる。

GraphQL のエラーハンドリング #

サーバのリソルバからクライアントにエラーを返すときにどうするのが良いか?

  • GraphQLError を throw してそれを返す
  • エラーを意味するスキーマを定義してそれを返す

の2つが考えられる。

前者のほうが GraphQL の考える標準的なハンドリング方法ではあるが、これだとクライアント側からどのようなエラーが発生しうるかの詳細が確認しづらくなる。スキーマとして定義する後者のほうが呼び出す側からは分かりやすそう。

クライアント側からリクエストする際の書き方 #

import { gql, GraphQLClient } from 'graphql-request';

const query1 = gql`
  # 3種類の書き方があるが、サーバーのログ記録などを考えると省略しない一番最初の書き方が好ましそう。

  # 1. クエリ操作(query 部分)および操作名(getBooks 部分)どちらも記述する書き方
  query getBooks {
    books {
      title
      author
    }
  }

  # 2. クエリ操作だけを記述する書き方
  # query {
  #   books {
  #     title
  #     author
  #   }
  # }

  # 3. どちらも省略する書き方 <- anonymous operation というらしい
  # {
  #   books {
  #     title
  #     author
  #   }
  # }
`;

const mutation1 = gql`
  mutation {
    greet
  }

  # anonymous operation にすると Query として扱われるので Mutation に以下のような書き方はない
  # {
  #   greet
  # }
`;

const mutation2 = gql`
  # 引数ありで、戻り値を受け取るものはこういう風に書く

  mutation {
    signIn(id: "abcde-12345") {
      name
    }
  }
`;

const mutation3 = gql`
  # 戻り値を受け取らないものはこういう風に書く

  mutation {
    signOut(id: "abcde-12345")
  }
`;

function requestToGraphql() {
  const client = new GraphQLClient('http://localhost:4000/graphql', { headers: {} });
  client.request(query1).then((data) => console.log(data));
  client.request(mutation1).then((data) => console.log(data));
  client.request(mutation2).then((data) => console.log(data));
  client.request(mutation3).then((data) => console.log(data));
}

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

個人的にはスキーマファーストのほうがあるべき形なのかなという印象。

コードファーストで行く場合、フロントエンドの人がスキーマを触るのが難しくないか?バックエンドの言語を使える必要がありそうだし。スキーマを介してコミュニケーションするということであれば、GraphQL だけを理解していれば良いという意味ではスキーマファーストのほうがあるべき姿と言えはしまいか。

その他 GraphQL の言語仕様 #

この記事が分かりやすかった。

考慮事項であったりベストプラクティスであったり #