GraphQL 再入門

モチベーション
GraphQLについて、「見聞きはするけど触ったことはないし、議論されているのを見ても判断基準がないので、自分なりの意見がないのがモヤモヤする」ぐらい。
とりあえず0 -> 1を学習して、どんなものか理解しておく。
個人的理解
「GraphQLはAPIのためのクエリ言語です」と聞いてもサッパリだが、「GraphQLという仕様があり、それに沿ってサーバー・クライアントを実装すると、GraphQLのクエリをRequestするだけで、必要なだけ値がResponseされる」という理解。
GraphQLの仕様に従えば、言語は何でも良いので、色々な実装がある。
一時期は GraphQL = Apollo Server / Apollo Client ぐらいの認識だったが、GraphQL Yoga / urql などがあるらしい。

GitHub GraphQL API を叩いてみる
エクスプローラで試す
外部APIでGraphQL APIになっているものの一つに、GitHub GraphQL API がある。これを叩いてみる。
一旦エクスプローラがあるので、それで試してみる。
初期状態からクエリに bio
を追加すると、Responseに bio
が追加されているのが分かる。
query {
viewer {
login
bio
}
}
{
"data": {
"viewer": {
"login": "ryokryok",
"bio": "write code scrap"
}
}
}
variablesとかあるが、ひとまず置いておいて、必要なクエリを指定すれば必要な分だけ取れることは確認。

urql でクライアントサイドから GitHub GraphQL API 叩いてみる
GitHub の管理画面から Fine-grained personal access tokens から Generate new token
でTokenを生成しておく。
プロジェクトセットアップ
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
VITE_GITHUB_API_KEY="<YOUR_API_KEY>"
型定義にも追加しておく。
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_GITHUB_API_KEY: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}
urqlをインストールする
pnpm install urql graphql
先ほどエクスプローラで取得したような値を取得してみる。
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で型をつけていくステップを追加予定。

VS Codeでの開発体験を改善する
- GraphQLの箇所にSyntax Highlightが欲しい
- GraphQLのスキーマを書くときに入力補完が欲しい
- クエリで得られる結果に対してTypeScriptの型が欲しい
1.に関しては下記のプラグインを導入することで対応できる。2.もこのプラグインが必要だが、さらに追加設定が必要。
3. は次項で説明予定。
GraphQL ConfigでGraphQL Schemaを認識するように設定する
下記のプラグインを入れるとGraphQL LSPが起動して、入力補完が効くようになる。
プラグインを入れるだけではダメで、読み込むSchemaを指定する必要がある。
graphql.config.ts
の設定
TSで書けた方が何かと便利なので、TSで書く。型定義のために graphql-config
を入れる。
pnpm add -D graphql-config
touch 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
の名前で置いておく。
設定が成功するとGraphQLのクエリを書く際に、入力補完が効くようになる。
もし入力補完が効かない場合はVS Codeを再起動するか、Shift + Command + P で @command:vscode-graphql.restart
を実行する。
GraphQL schema が手元にない場合は?
-
schema
にAPIのURLを指定する - GraphQLのSchemaをダウンロードする
1はローカルでGraphQLサーバーを走らせている、サーバーがCode Firstで実装されてSchemaがない場合などは有効。
2はダウンロードした時点のスナップショットになるが、サーバーとの通信が発生しないので早い。長く開発するなら、できればこっちをファーストチョイスにしたい。
そんな例を下記のGistで書いた。

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としてまとめられている。それを使う。
pnpm add -D @graphql-codegen/cli @graphql-codegen/client-preset @parcel/watcher
# 公式の手順ではないが、インストールしないと型補完が効かないため入れる
pnpm add -D @graphql-typed-document-node/core
Codegen用の設定ファイル 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モードの理由は後述する。
"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
にて作成した型を利用するように置き換える。
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
に型がつくようになる。型が作成されて読み込まれるまで少しラグがある。
Codegen Client Presetの仕組み
マジカルな挙動に見えるが、こんな感じ
- CodegenがWatchモードで起動し、GraphQLのクエリが書かれるファイルを監視する
-
import { graphql } from "./gql";
のgraphql
にて、オペレーション名があるクエリを書くと、Codegenが対応したオペレーション名を元に型を生成する
- オペレーション名 =
query
横に記載された名前、今回の場合はgetViewer
- 生成した型が読み込まれ、
ViewerQuery
に型がつく -
useQuery
で型推論されてdata
に型がつく
オペレーション名がないクエリを書いた場合は型生成がスキップされる。
[client-preset] the following anonymous operation is skipped:
query {
import type { ... }
にしてほしい
おまけ: 自動生成されたファイルも codegen.ts
で useTypeImports
を追加する。
const config: CodegenConfig = {
schema: "./schema.docs.graphql",
documents: ["./src/**/*.{graphql,js,ts,jsx,tsx}"],
ignoreNoDocuments: true,
generates: {
"./src/gql/": {
preset: "client",
+ config: {
+ useTypeImports: true,
+ },
},
},
};

GraphQL の Arguments と Variables をサクッと理解する
再度エクスプローラーを開く。
GraphQLにも当然RESTの /posts/:id
の:id
のような形式で、取得するリソースに対して引数を指定するケースもある。
例えばリポジトリのIssuesを取得するケースの場合、リポジトリの owner
と name
を指定する必要がある。
例として 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 の部分。
エディター上で型定義を確認できる。
今回の様に引数を固定ではなく、例えば urql-graphql/urql
の場合も取れる様にしたい。その場合、 Variables で動的に指定できる様にする。
- 先ほどの
GetRepositryIssues
の引数として$owner: String!
$name: String!
を定義する。 - 固定値で書いていた部分を引数にする。
repository(owner: $owner, name: $name)
-
$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"
}
エディター上では補完が効く。
これで動的に値を変更できる様になった。別のリポジトリを対象にしたい場合は Variables を変えればいい。
{
"owner": "urql-graphql",
"name": "urql"
}

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>
);
}
こんな感じで型定義が表示される。

GraphQL Yoga でサーバーを建てる
このチュートリアルをやってみたが、多分理解するのにこれをやるのが一番早い。非常に良いチュートリアル。
技術スタックとしては下記
- GraphQL Yoga
- Prisma
- SQLite
ローカルで完結するし、ステップアップが丁寧で分かりやすい。GraphQLについて、最低限おさらいしながら実装していく。
詰まったらチュートリアルの作者がリポジトリをアップしてくれているのでそれ参考にすれば良さそう。