Open8

GraphQL 再入門

mr_ozinmr_ozin

モチベーション

GraphQLについて、「見聞きはするけど触ったことはないし、議論されているのを見ても判断基準がないので、自分なりの意見がないのがモヤモヤする」ぐらい。

とりあえず0 -> 1を学習して、どんなものか理解しておく。

個人的理解

「GraphQLはAPIのためのクエリ言語です」と聞いてもサッパリだが、「GraphQLという仕様があり、それに沿ってサーバー・クライアントを実装すると、GraphQLのクエリをRequestするだけで、必要なだけ値がResponseされる」という理解。

GraphQLの仕様に従えば、言語は何でも良いので、色々な実装がある。

一時期は GraphQL = Apollo Server / Apollo Client ぐらいの認識だったが、GraphQL Yoga / urql などがあるらしい。

mr_ozinmr_ozin

GitHub GraphQL API を叩いてみる

エクスプローラで試す

外部APIでGraphQL APIになっているものの一つに、GitHub GraphQL API がある。これを叩いてみる。

一旦エクスプローラがあるので、それで試してみる。

https://docs.github.com/ja/graphql/overview/explorer

初期状態からクエリに bio を追加すると、Responseに bio が追加されているのが分かる。

query {
  viewer {
    login
    bio
  }
}
{
  "data": {
    "viewer": {
      "login": "ryokryok",
      "bio": "write code scrap"
    }
  }
}

variablesとかあるが、ひとまず置いておいて、必要なクエリを指定すれば必要な分だけ取れることは確認。

mr_ozinmr_ozin

urql でクライアントサイドから GitHub GraphQL API 叩いてみる

GitHub の管理画面から Fine-grained personal access tokens から Generate new token でTokenを生成しておく。

https://github.com/settings/personal-access-tokens

プロジェクトセットアップ

pnpm create vite react-github-api-graphql --template react-ts
cd react-github-api-graphql
pnpm install

不要なファイルは削除する。

rm -rf src/assets src/App.css src/index.css public

先ほど取得したGitHub API Keyを .env に追記する。Clientサイドに埋め込まれるので公開しないこと。間違ったらAPIをRegenerateする。

touch .env
.env
VITE_GITHUB_API_KEY="<YOUR_API_KEY>"

型定義にも追加しておく。

src/vite-env.d.ts
/// <reference types="vite/client" />

interface ImportMetaEnv {
	readonly VITE_GITHUB_API_KEY: string;
}

interface ImportMeta {
	readonly env: ImportMetaEnv;
}

urqlをインストールする

pnpm install urql graphql

先ほどエクスプローラで取得したような値を取得してみる。

src/App.tsx
import {
	Client,
	Provider,
	cacheExchange,
	fetchExchange,
	gql,
	useQuery,
} from "urql";

// 1. define client
const client = new Client({
	url: "https://api.github.com/graphql",
	exchanges: [cacheExchange, fetchExchange],
	fetchOptions: {
		headers: { Authorization: `bearer ${import.meta.env.VITE_GITHUB_API_KEY}` },
	},
});

// 2. define query
const ViewerQuery = gql(/* GraphQL */ `
    query {
      viewer {
        login
        bio
      }
    }
`);

// 3. write component
const Viewer = () => {
	const [result] = useQuery({
		query: ViewerQuery,
	});

	const { data, fetching, error } = result;

	if (fetching) return <p>Loading...</p>;
	if (error) return <p>Oh no... {error.message}</p>;
	if (data.viewer) return <pre>{JSON.stringify(data.viewer, null, 2)}</pre>;
};

// 4. Wrap components with Provider
function App() {
	return (
		<Provider value={client}>
			<Viewer />
		</Provider>
	);
}

export default App;

成功すると GitHub のID、プロフィールのbioが表示される。

{
  "login": "ryokryok",
  "bio": "write code scrap",
  "__typename": "User"
}

今日は一旦終わり、後日Codegenで型をつけていくステップを追加予定。

mr_ozinmr_ozin

VS Codeでの開発体験を改善する

  1. GraphQLの箇所にSyntax Highlightが欲しい
  2. GraphQLのスキーマを書くときに入力補完が欲しい
  3. クエリで得られる結果に対してTypeScriptの型が欲しい

1.に関しては下記のプラグインを導入することで対応できる。2.もこのプラグインが必要だが、さらに追加設定が必要。
3. は次項で説明予定。

https://marketplace.visualstudio.com/items?itemName=GraphQL.vscode-graphql

GraphQL ConfigでGraphQL Schemaを認識するように設定する

下記のプラグインを入れるとGraphQL LSPが起動して、入力補完が効くようになる。

https://marketplace.visualstudio.com/items?itemName=GraphQL.vscode-graphql

プラグインを入れるだけではダメで、読み込むSchemaを指定する必要がある。

https://github.com/graphql/graphiql/tree/8711cceec1e040d146aacb25fe41330e167852c2/packages/vscode-graphql#simplest-config-example

graphql.config.ts の設定

TSで書けた方が何かと便利なので、TSで書く。型定義のために graphql-config を入れる。

pnpm add -D graphql-config
touch graphql.config.ts
graphql.config.ts
import type { IGraphQLConfig } from "graphql-config";

const config: IGraphQLConfig = {
	schema: ["./schema.docs.graphql"],
	documents: ["./src/**/*.{graphql,js,ts,jsx,tsx}"],
};

export default config;

GitHub GraphQL API の Schemaをダウンロードする

下記のページからダウンロードできる。プロジェクトのルートに schema.docs.graphql の名前で置いておく。

https://docs.github.com/en/graphql/overview/public-schema

設定が成功するとGraphQLのクエリを書く際に、入力補完が効くようになる。

VS CodeでGraphQLのAutocompleteが効いている
画面

もし入力補完が効かない場合はVS Codeを再起動するか、Shift + Command + P で @command:vscode-graphql.restart を実行する。

GraphQL schema が手元にない場合は?

  1. schema にAPIのURLを指定する
  2. GraphQLのSchemaをダウンロードする

1はローカルでGraphQLサーバーを走らせている、サーバーがCode Firstで実装されてSchemaがない場合などは有効。

2はダウンロードした時点のスナップショットになるが、サーバーとの通信が発生しないので早い。長く開発するなら、できればこっちをファーストチョイスにしたい。

そんな例を下記のGistで書いた。

https://gist.github.com/ryokryok/352822a8e240ec00deb162563bd3c233

mr_ozinmr_ozin

CodegenでGraphQLからTypeScriptの型を生成する

ここで先ほどのコードをちょっと簡略したものを再掲する。

Queryから取得したデータの型がAny型になる。

// 2. define query
const ViewerQuery = gql(`
    query {
      viewer {
        login
        bio
      }
    }
`);

// 3. write component
const Viewer = () => {
    // data は any 型
	const [{data}] = useQuery({
		query: ViewerQuery,
	});

	if (data.viewer) return <pre>{JSON.stringify(data.viewer, null, 2)}</pre>;
};

手作業で型を書いたり、ZodでValidateする方法があるが、GraphQLと二重定義になってしまう。

幸いなことに、GraphQLのSchemaファイルから色々生成するCodegenというツールがある。

今回はそれでクライアント向けのTypeScriptの型を生成する。

codegen.ts with Client preset の設定

前の手順schema.docs.graphql がプロジェクトルートに配置されている前提。

Codegenでは色んな用途に向けて設定をカスタマイズできるが、今回のようなTypeScriptで構築されたクライアントサイドアプリに必要な型を出力するプリセットがClient presetとしてまとめられている。それを使う。

https://the-guild.dev/graphql/codegen/plugins/presets/preset-client

pnpm add -D @graphql-codegen/cli @graphql-codegen/client-preset @parcel/watcher
# 公式の手順ではないが、インストールしないと型補完が効かないため入れる
pnpm add -D @graphql-typed-document-node/core

Codegen用の設定ファイル codegen.ts を作成する。

codegen.ts
import type { CodegenConfig } from "@graphql-codegen/cli";

const config: CodegenConfig = {
	schema: "./schema.docs.graphql",
	documents: ["./src/**/*.{graphql,js,ts,jsx,tsx}"],
	ignoreNoDocuments: true,
	generates: {
		"./src/gql/": {
			preset: "client",
		},
	},
};

export default config;

Codegenで型を生成する

コマンドを追加する。Watchモードの理由は後述する。

package.json
	"scripts": {
		"generate": "graphql-codegen --watch"
	},

実行する。

pnpm generate

実行すると src/gql にCodegenで作成された型がある。この段階では、「なんか生成されている」程度でOK。

$ tree src/gql    
src/gql
├── fragment-masking.ts
├── gql.ts
├── graphql.ts
└── index.ts

1 directory, 4 files

作成した型を利用する

App.tsx にて作成した型を利用するように置き換える。

App.tsx
import {
	Client,
	Provider,
	cacheExchange,
	fetchExchange,
-	gql,
	useQuery,
} from "urql";
+ import { graphql } from "./gql";

// 1. define client
//

// 2. define query
- const ViewerQuery = gql(`
+ const ViewerQuery = graphql(`
-     query {
+     query getViewer{
      viewer {
        login
        bio
      }
    }
`);

// 3. write component
//

// 4. Wrap components with Provider
//

export default App;

型生成が上手くいくと data に型がつくようになる。型が作成されて読み込まれるまで少しラグがある。

VS Code

Codegen Client Presetの仕組み

マジカルな挙動に見えるが、こんな感じ

  1. CodegenがWatchモードで起動し、GraphQLのクエリが書かれるファイルを監視する
  2. import { graphql } from "./gql";graphql にて、オペレーション名があるクエリを書くと、Codegenが対応したオペレーション名を元に型を生成する
  • オペレーション名 = query 横に記載された名前、今回の場合は getViewer
  1. 生成した型が読み込まれ、ViewerQuery に型がつく
  2. useQuery で型推論されて data に型がつく

オペレーション名がないクエリを書いた場合は型生成がスキップされる。

[client-preset] the following anonymous operation is skipped: 
    query {

おまけ: 自動生成されたファイルも import type { ... } にしてほしい

codegen.tsuseTypeImports を追加する。

codegen.ts
const config: CodegenConfig = {
	schema: "./schema.docs.graphql",
	documents: ["./src/**/*.{graphql,js,ts,jsx,tsx}"],
	ignoreNoDocuments: true,
	generates: {
		"./src/gql/": {
			preset: "client",
+			config: {
+				useTypeImports: true,
+			},
		},
	},
};
mr_ozinmr_ozin

GraphQL の Arguments と Variables をサクッと理解する

再度エクスプローラーを開く。

https://docs.github.com/ja/graphql/overview/explorer

GraphQLにも当然RESTの /posts/:id:id のような形式で、取得するリソースに対して引数を指定するケースもある。
例えばリポジトリのIssuesを取得するケースの場合、リポジトリの ownername を指定する必要がある。
例として facebook/react のIssueを取りたい。するとこんな感じで書ける。

query GetRepositryIssues{
  repository(owner: "facebook", name: "react"){
    issues(first: 10, states: OPEN) {
      edges {
        node {
          title
          url
          createdAt
        }
      }
    }
  }
}

repository(owner: "facebook", name: "react") が Arguments の部分。
エディター上で型定義を確認できる。

GraphQL Arguments

今回の様に引数を固定ではなく、例えば urql-graphql/urql の場合も取れる様にしたい。その場合、 Variables で動的に指定できる様にする。

  1. 先ほどの GetRepositryIssues の引数として $owner: String! $name: String! を定義する。
  2. 固定値で書いていた部分を引数にする。 repository(owner: $owner, name: $name)
  3. $owner: String! $name: String! に対応する Variables を指定する。
query GetRepositryIssues($owner: String!, $name: String!){
  repository(owner: $owner, name: $name){
    issues(first: 10, states: OPEN) {
      edges {
        node {
          title
          url
          createdAt
        }
      }
    }
  }
}

Variables 自体はJSON形式で書くことができる。

{
  "owner": "facebook",
  "name": "react"
}

エディター上では補完が効く。

GraphQL Variables

これで動的に値を変更できる様になった。別のリポジトリを対象にしたい場合は Variables を変えればいい。

{
  "owner": "urql-graphql",
  "name": "urql"
}
mr_ozinmr_ozin

urql で Variables を指定する

Codegen がクエリを監視して、 Variables に対しても型を生成してくれる。

urql では useQuery の引数で指定できる。

const RepositoryIssuesQuery = graphql(`
  query GetRepositoryIssues($owner: String!, $name: String!) {
    repository(owner: $owner, name: $name) {
      issues(first: 10, states: OPEN) {
        edges {
          node {
            title
            url
            createdAt
          }
        }
      }
    }
  }
`);

const RepositoryIssues = () => {
	const [{ data }] = useQuery({
		query: RepositoryIssuesQuery,
		variables: { owner: "facebook", name: "react" },
	});

	return <pre>{JSON.stringify(data, null, 2)}</pre>;
};

function App() {
	return (
		<Provider value={client}>
			<RepositoryIssues />
		</Provider>
	);
}

こんな感じで型定義が表示される。

mr_ozinmr_ozin

GraphQL Yoga でサーバーを建てる

このチュートリアルをやってみたが、多分理解するのにこれをやるのが一番早い。非常に良いチュートリアル。

https://the-guild.dev/graphql/yoga-server/tutorial/basic

技術スタックとしては下記

  • GraphQL Yoga
  • Prisma
  • SQLite

ローカルで完結するし、ステップアップが丁寧で分かりやすい。GraphQLについて、最低限おさらいしながら実装していく。

詰まったらチュートリアルの作者がリポジトリをアップしてくれているのでそれ参考にすれば良さそう。

https://github.com/Urigo/hackernews-node-ts