2023年1月11日 更新

GraphQLはいいぞというお話

GraphQLとは

" GraphQL は、API のためのクエリ言語であり、既存のデータを使ってクエリを実行するためのランタイムです。GraphQL は、API 内のデータについて完全で理解しやすい記述を提供し、クライアントに必要なものだけを要求し、時間の経過とともに API を進化させることを容易にし、強力な開発者ツールを可能にします。"
via GraphQL


公式サイトではGraphQLについて上記の説明がされています。
要約するとAPIへの問い合わせを単純化し、使いやすくしているということです。    
クライアント側・サーバー側両方で使用可能で多くの言語をサポートしています。  
Code using GraphQL

GraphQLの主な特徴は以下です。

- エンドポイントは1つ
- オーバーフェッチング(余計なデータを取得)せずに済む
- 型指定でデータが明確になる
- GUIで操作できる開発者ツール(GraphQL IDE)

GraphQLサーバー概要図

REST APIとの違い

GraphQLはREST APIと比較されることが多いです。  
REST APIはエンドポイントをサーバー側に設定してエンドポイントに問い合わせが来たら指定のデータを返します。エンドポイント1つ1つに対してリクエストを送る必要があります。対してGraphQLはエンドポイントは1つです。GraphQL内のschema(スキーマ)とresolver(リゾルバー)に指定の設定をすることにより必要なデータを取得します。 エンドポイントを1つにすること、schemaとresolverによる柔軟なデータ取得構造によりリクエスト過多やオーバーフェッチングといったパフォーマンス面の改善が可能になります。

REST APIとの比較は以下で詳細に述べられているのでご参照ください。  
GraphQLとRESTの比較 - HUSURA

REST APIとの比較図

REST APIとの比較図

GraphQLの使い方

GraphQLパッケージをインストールして使用します。  
言語としてJavaScript/TypeScriptを選択する際は、サーバー側はNode.jsフレームワークと組み合わせて使用するパターンが多いです。クライアント側はReactやVueなどのJavascriptフレームワークのエコシステムとして使用されます。  
代表的なパッケージは以下です。

サーバー  
- Apollo Server
- Express GraphQL
- Mercurius

クライアント
- Apollo Client
- urql


今回はTypeScriptの使用を前提に、ExpressでAPIサーバーを作成したのでこちらを例に説明します。  

※ ExpressやTypeScriptの詳細設定については省略します。  

GraphQLはschemaの定義が必要になります。schemaとはデータ構造で型を定義します。resolverはschemaで定義した型に対して何かしら実体のある値を設定する関数になります。流れとしてはschemaを定義→それに対するresolve関数を設定→APIを叩くといった流れになります。

schemaで定義するQueryとMutationは予約語で、それぞれ取得や登録といった役割を持っています。SQLと比較すると以下になります。


取得:SQL→SELECT  GraphQL→Query
登録:SQL→INSERT  GraphQL→Mutation
更新:SQL→UPDATE  GraphQL→Mutation
削除:SQL→DELETE  GraphQL→Mutation

サーバーを立ち上げるとGraphQL IDEにて挙動を確認することができます。
postmanのGraphQL版のようなイメージです。
画面左側がクエリ情報、右側はレスポンスになります。  
また、画面の一番右側のようにschemaで定義した内容を確認することができるので、ドキュメントとしても使用可能です。

GraphQL IDE

必要なパッケージ

以下のパッケージをインストールします。


yarn add express graphql
yarn add -D @types/express @types/node @graphql-yoga/node npm-run-all rimraf ts-node-dev typescript


.


- graphql  
graphqlの本体です。

- @graphql-yoga/node  
GraphQL Yogaは、Envelop(GraphQLプラグインシステム)をエンジンに動作するHTTP仕様準拠のGraphQL Serverです。  
Envelopは認証やキャッシュ、エラー処理を容易にしてくれます。
実行可能なschemaとresolverをYogaに提供することで動作します。

APIの実装

実装内容は値の取得(2パターン)と追加になります。  
それぞれGraphQL IDE(localhost:8000/graphql)での実行結果は以下になります。

infoメソッド実行結果

postメソッド実行結果

feedメソッド実行結果


import express from "express";
import { createServer } from "@graphql-yoga/node";
import resolvers from "./resolver";
import fs from "fs";
import path from "path";

const app = express();
const PORT = 8000;


// ポイント1
const server = createServer({
schema: {
typeDefs: fs.readFileSync(path.join(__dirname, "schema.graphql"), "utf-8"),
resolvers,
},
graphiql: true,
});

app.use("/graphql", server);

try {
app.listen(PORT, () => {
console.log(
`Running a GraphQL API server at http://localhost:${PORT}/graphql`
);
});
} catch (e) {
if (e instanceof Error) {
console.error(e.message);
}
}


app.ts

"GraphQLスキーマの定義"
// ポイント2
type Query {
info: String!
feed: [Link]!
}

type Mutation {
post(url: String!, description: String!): Link!
}

type Link {
id: ID!
description: String!
url: String!
}


schema.graphql

// ダミーデータ
let links = [
{
id: "link-0",
description: "aaa",
url: "www.aaa.com",
},
];

// ポイント3
const resolvers = {
Query: {
info: () => "HackerNewsクローン",
feed: () => links,
},
Mutation: {
// ポイント4
post: (_parent: any, args: { description: string; url: string; }) => {
let idCount = links.length;

const link = {
id: `link-${idCount++}`,
description: args.description,
url: args.url,
};

links.push(link);
return link;
},
},
};

export default resolvers;


resolver.ts

Expressの基本的な記述そのままに、app.tsのserver関数内にGraphQLの必要な設定を行い、serverをミドルウェアとして呼び出しています。  
それぞれのポイントとなる部分を説明します。  

- ポイント1  
const serverにてcreateServerを定義します。schemaプロパティにてtypeDefs(schema)とresolvers(resolver)を定義します。前述の通りsheamaはGraphQLサーバーにて使用する型定義の集合であり、resolverはschemaで定義した型に対して何かしら実体のある値を設定する関数になります。
graphiqlはGraphQL IDEです。  
※ schema、resolver共に外部ファイルとして読み込んでいます。

- ポイント2  
schemaの設定ファイルになります。ファイルの拡張子は.graphqlを使用しています。  
QueryやMutation以外の型についても定義します。  

- ポイント3  
resolver関数群をまとめたファイルになります。  
schemaと同じくQuery、Mutationを設定し、定義した型に基づくメソッドを設定します。

- ポイント4  
postメソッドの引数に設定しています。parentは定義したschemaに親要素があればその親要素を呼び出すことができます。argsは関数実行時に引数に設定した値になります。その他に設定できる引数として、共通設定を呼び出すcontextや実行したオペレーションに関する状態等の詳細情報を持ったinfoがあります。

GraphQL Code Generatorによる型(TypeScript)の自動生成

TypeScriptでGraphQLを扱うと、resolver関数への型定義の内容がschemaで記述した内容と重複するため二度手間になります。また、schemaとresolver関数の型定義との間に不整合があると、エラーの原因になります。  
GraphQL Code Generatorは、上記の問題を解消できるライブラリです。
具体的にはschemaから型を生成することにより、実現することができます。
resolver関数には戻り値や引数に対する型注釈を手動で行う必要があります。
下記のresolversに型注釈を追加し、Mutation内のメソッドにも型が適応されるようにします。resolversに追加する型はschemaから自動生成します。


const resolvers = {
// 省略
Mutation: {
post: (_parent: any, args: { description: string; url: string; }) => {
// 省略
},
},
};


resolver.ts

必要なパッケージと設定ファイルの生成


yarn add -D @graphql-codegen/cli
yarn graphql-code-generator init


.

graphql-code-generator initを実行するといくつかの質問項目に回答し、完了するとcodegen.ymlが作成されます。codegen.ymlの内容に基づきschemaから型を生成します。schemaが対象ファイル、generateで出力先を指定します。


overwrite: true
schema: "src/schema.graphql"
generates:
src/types/graphql.d.ts:
config:
useIndexSignature: true
plugins:
- "typescript"
- "typescript-resolvers"


codegen.yml

型定義ファイルの生成

以下を実行し型定義ファイルを生成します。


yarn graphql-codegen --config codegen.yml


.

実行が完了するとcodegen.ymlで指定したディレクトリに型定義ファイルが生成されます。生成されたファイルをインポートし、resolverに型注釈を追加します。


import { Resolvers } from "./types/graphql";

// resolver
const resolvers: Resolvers = { // Resolversの型注釈を追加
// 省略
Mutation: {
post: (_parent, args) => { // 引数に型注釈が必要ない
let idCount = links.length;

const link = {
id: `link-${idCount++}`,
description: args.description,
url: args.url,
};

links.push(link);
return link;
},
},
};


resolver.ts

型注釈を追加したことにより、postメソッドの引数に関しても型推論が働くようになりました。Resolversを追加しないとpostメソッドの引数はエラーになります。  
また、スキーマに変更があった場合もコマンド一つで既存の型定義を更新してくれます。  
今回はバックエンド側から使用しましたが、フロントエンド側でGraphQLを呼び出す時にクエリに渡すパラメータやモデル、レスポンス内容といったTypeScript型定義をGraphQLスキーマから生成可能です。  
schemaファイルさえあればフロントエンド、バックエンド共に型を自動生成することが可能です。型を拡張など変更したい際もschemaファイルさえ変更すればよく、API設計書を右往左往して該当部分をいくつも修正する必要がなくなります。つまり管理がとても容易になります。
schemaから型を自動生成するメリットは沢山あり、GraphQLを使用する上で最も開発者体験の高さを実感したポイントでした。

まとめ

GraphQLはパフォーマンスの向上や、型定義の恩恵が受けられるという点で今後も積極的に使用したい技術だと実感しました。prismaなどのORMと組み合わせることでより便利に使えるのではとも考えています。

今回はバックエンドの実装例でしたが、フロントエンドでの使用も試してみる予定です。  今後はNest.js + prismaを組み合わせたケースなどのナレッジをお伝えしていきたいと思います。

参考資料
GraphQL Code Generator で TypeScript の型を自動生成する
GraphQL Server の実装

※掲載内容は個人の見解です。
※会社名、製品名、サービス名等は、各社の登録商標または商標です。