GraphQL の基礎を理解し、Apollo や GraphQL Code Generator を使える様になるまでの勉強の流れ
何度か勉強したけどいまいちわからずのままなので、勉強したことをここに書き残す。
目標
概念を理解して何か作ってみる
Firebase が好きなので DB は Firestore を使ってみたいけどどうなんだろう
現状
Apollo GraphQL とか GraphQL Code Generator とかを使って Next.js の上に GraphQL サーバーを立ててフロントから使えるようにはできたけど、全く意味が理解できていない!
いきなり大きい構成でやっているのが悪い気がするので小さいサンプルから学んでいく
やること
まずは周辺の単語を理解したい。
初めて出会うワードが結構あるのでそれぞれがどういう意味なのかを理解する。
英語の勉強もかねて自力で翻訳しながら読んでみる
何はともあれ公式ドキュメント
いろいろな組み合わせの使い方が用意されている...すごい...
自分は TypeScript
+ Apollo
を見るのが良さそうだけど、一旦公式ドキュメントを先に見てから
Query
どのデータが欲しいか要求する時に使うもの
{
hero {
name
}
}
Auguments
引数が使えるので特定のデータを取得したい時とかに使う
{
hero(id: "1000") {
name
}
}
Aliases
同じフィールドを違う引数で取得したい時とかに名前をつけることができる
{
empireHero: hero(episode: EMPIRE) {
name
}
jediHero: hero(episode: JEDI) {
name
}
}
Fragments
上記のように同じものを何度も記述すると複雑になっていくので、再利用可能なユニットとして定義ができる
{
leftComparison: hero(episode: EMPIRE) {
...comparisonFields
}
rightComparison: hero(episode: JEDI) {
...comparisonFields
}
}
fragment comparisonFields on Character {
name
appearsIn
friends {
name
}
}
Inline Fragments
フラグメントを定義しなくてもインターフェースやユニオン型を使って欲しいフィールドの制御ができる
以下の場合、引数が JEDI
だと Droid
なので primaryFunction
が返されるが、引数が EMPIRE
だと Human
なので height
が返される
query HeroForEpisode($ep: Episode) {
hero(episode: $ep) {
name
... on Droid {
primaryFunction
}
... on Human {
height
}
}
}
↓
{
"ep": "JEDI"
}
Operation name
ここまでの例では省略していた query
キーワードやクエリ名のこと.
query
, mutation
, subscription
がある
デバッグなどで使うのに便利なので使うのを推奨しているみたい
query HeroNameAndFriends {
hero {
name
friends {
name
}
}
}
Variables
欲しいクエリを都度ベタ書きするのは実務では使いづらいので、動的にしたい部分をクエリから抜き出して map として渡すことができる
- 変数は
$
から始まるのがルール -
($episode: Episode)
の右側は型の指定。scalars
,enums
,input types
を指定できる。この場合は自作の enum の Episode型 -
($episode: Episode = JEDI)
のようにデフォルト引数も使える
query HeroNameAndFriends($episode: Episode) {
hero(episode: $episode) {
name
friends {
name
}
}
}
↓
{
"episode": "JEDI"
}
Directives
引数だけでは解決できない構造の動的変更のために使えるもの
-
@inlucde(if : Boolean)
引数がtrue
の時のみフィールドを含める -
@skip(if : Boolean)
引数がtrue
の時のみフィールドを含めない
query Hero($episode: Episode, $withFriends: Boolean!) {
hero(episode: $episode) {
name
friend @include(if: $withFriends) {
name
}
}
}
↓
{
"episode": "JEDI"
"withFriend": false
}
Meta fields
GraphQL サービスから返される型がわからないときには __typename
を使うとオブジェクトの型名を取得できる
{
search(text: "an") {
__typename
... on Human {
name
}
... on Driod {
name
}
... on Starship {
name
}
}
}
↓こんな感じのデータが返ってくる
{
"data": {
"search": [
{
"__typename": "Human",
"name": "Han Solo"
},
{
"__typename": "Human",
"name": "Leia Organa"
},
{
"__typename": "Starship",
"name": "TIE Advanced x1"
}
]
}
}
Mutations
データを更新するためのリクエストを定義するもの
-
GET
→query
-
POST
→mutation
-
PATCH
→mutation
-
DELETE
→mutation
みたいな感じ。
REST でも同じだけど、サーバー側の実装によってはさまざまな副作用が起きる可能性はあるので、query
で更新できないとか、mutation
じゃないと更新ができないとかではなく、あくまでルール的なものとして捉えるのが良さそう。
ただ、query
として実行したものは並列処理されるから早いらしいが、mutation
として実行したものは値の整合性を保つために順次一つ一つ実行されるので遅くなるっぽい。
https://graphql.org/learn/queries/#mutations:~:text=While query fields are executed in parallel%2C mutation fields run in series%2C one after the other.
また、GraphQL は全部 POST
らしい
厳密には GET
でもできるけどクエリ文字列をURLに含める感じになる
おそらくPOST
で本文にクエリ文字列を含む形が一般的
参考 : https://graphql.org/learn/serving-over-http/#post-request
実際にはこんな感じ
mutation CreateReviewForEpisode($ep: Episode, $review: TreviewInput!) {
createReview(episode: $ep, review: $review) {
stars
commentary
}
}
↓
{
"ep": "JEDI"
"review": {
"stars": 5,
"commentary": "This is a great movie!"
}
}
一度のリクエストでデータの更新をして、そのレスポンスで更新されたデータが返ってくるみたいにできて便利みたい。
ちょうど REST で API を作ってサービスを作ろうとしてた時に、更新後にまたデータとってくるリクエストしないといけないのどうしようかなと思ってたので、それが一発でいい感じになるよって感じかな
概念の話
GraphQL はクエリ言語と呼ばれている。
エンドポイントが一つだけで、リクエスト時に投げるクエリによって取得するデータの構造などを決めることができる。
REST API みたいに機能やページごとでエンドポイントを切っている場合は特定のAPIにフィールドを追加して欲しい時とかにバックエンド側への依頼が必要になるが、GraphQLならフロントエンド側でクエリを書き換えれば取得できる内容が変わるので特にコミュニケーションが不要になる。
また、REST の場合は使わないデータも毎回取得してしまうことになるが、GraphQL であれば必要な時に必要なデータだけ取得することができる。
実装は大きく3つに分かれる
概念の理解でややこしいのが全体像が見えてこないことな気がしている。
まだ完全にわかったわけではないけど、おそらくこの3つが大きな柱になるところ。
スキーマ定義
バックエンドの実装
フロントエンドの実装
今の理解だとまずはスキーマ定義をして、それをもとにバックエンドの実装とフロントエンドの実装をそれぞれ行う流れになるだと思う。
なんか全部いい感じになるんでしょ?ってことはなくて、それぞれちゃんと定義や実装が必要なはず
Apollo とか、GraphQL Code Generator を使えば、サーバーの立ち上げが簡単だったり、スキーマからコードが自動で生成されるようにできたりして開発が楽にできるみたいなイメージ(どこまで自動化できるんだろうか)
理解が難しい点としては上記のような便利なツールがたくさんあって、それらのどれかを使って実装するのがスタンダードになっているので、GraphQL 自体が何を持っていて何を指しているのか、Apollo は何をしてくれるのかとかが分かりづらくなっているのかなと思った。
個人的にも Apollo で動かすところまでは簡単にいったけど、よくわからなくて一番シンプルな採用単位の GraphQL のサンプルを読んでいるところ
GraphQL のみで動かす最も小さいサンプル
Apollo などのツールを使わずに GraphQL サーバーを動かす最小の単位
管理のしやすさとか型定義とか一切気にしなくてよければ、実務で使えるレベルの最小の構成はこんなかんじかなと思います
参考: https://graphql.org/graphql-js/running-an-express-graphql-server/
const express = require('express');
const { graphqlHTTP } = require('express-graphql');
const { buildSchema } = require('graphql');
// スキーマを定義
const schema = buildSchema(`
type Query {
hello: String
}
`);
// クライアントから叩かれたら動く関数を定義
const root = {
hello: () => {
// 実際にはここでDBに接続してデータを取得して返す
return 'Hello world!';
},
};
const app = express();
app.use('/graphql', graphqlHTTP({
schema: schema,
rootValue: root,
graphiql: true,
}));
app.listen(4000);
console.log('Running a GraphQL API server at http://localhost:4000/graphql');
こんな感じで /graphql
へのルーティングを作って GraphQL を動くようにすればいいだけなので express
は必須ではないですが、色々楽なので入れるの前提が良い気がする。(そもそも Apollo Server とか使うからこの構成はやらない気がするけど)
root
変数の hello()
関数はクライアントからリクエストが来たら動く関数(resolver, リゾルバ)なので、ここでDBと接続してデータを取得して返したりするイメージ
これで http://localhost:4000/graphql
で叩けるようになるので、クライアント側はこのエンドポイントに対してクエリ文字列を持たせた POST
でリクエストすれば良い。
参考: https://graphql.org/graphql-js/graphql-clients/
const query = `{
hello
}`;
fetch('/graphql', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
body: JSON.stringify({
query,
})
})
.then(r => r.json())
.then(data => console.log(data)); //-> "Hello world!"
型について
Scalar types
GraphQL オブジェクトで使えるデフォルトで用意されている型
Int
Float
String
Boolean
ID
がある
カスタムスカラーとして自前で定義することもできる
scalar Date
参考: https://graphql.org/learn/schema/#scalar-types
Enum types
いわゆる列挙型
enum Episode {
NEWHOPE
EMPIRE
JEDI
}
List
フィールドがリストの場合は型名を [
と ]
で囲むことで表現できる
type Character {
friends: [String]
}
Null
型名の後ろに !
をつけると null
を許容しないことを宣言できる
この場合は String!
なので、確実に文字列が入ってくる
type Character {
name: String!
}
リストの場合は !
の位置で意味が変わる
type Character {
friends: [String!] # リストはnullの可能性はあるが、リストの中の値は確実に文字列
friends: [String]! # リストは必ずnullではないが、中身はnullも入ってくる可能性がある
}
Interfaces
Java とかみたいに型を実装するために必要なフィールドをまとめて定義する抽象型
定義した interface を implements
で実装することができる
また、interface に定義されたフィールド以外を追加することももちろんできる
interface Character {
id: ID!
name: String!
friends: [Character]
appearsIn: [Episode]!
}
type Human implements Character {
id: ID!
name: String!
friends: [Character]
appearsIn: [Episode]!
totalCredits: Int
}
type Droid implements Character {
id: ID!
name: String!
friends: [Character]
appearsIn: [Episode]!
primaryFunction: String
}
ただ、こんな感じで取得しようとすると型のエラーになることがある
query HeroForEpisode($ep: "JEDI") {
hero(episode: $ep) {
name
primaryFunction # Human にはこのフィールドはないです。というエラーになる
}
}
この場合は Inline Fragments を使えば回避できる
query HeroForEpisode($ep: "JEDI") {
hero(episode: $ep) {
name
... on Driod {
primaryFunction # Droid の時だけ取得するので Human の時は無視される
}
}
}
Union types
ユニオン型は「どれかしらの型と一致するもの」という型を作ることができる
インターフェースや他のユニオン型を使って定義することはできない
union SearchResult = Human | Droid | Starship
SearchResult
のリストを返すクエリをする場合、それぞれの型での変数の違いをいい感じに無視して取得したい時はこんな感じになる
{
search(test: "an") {
__typename
... on Human {
name
height
}
... on Droid {
name
primaryFunction
}
... on Starship {
name
length
}
}
}
これで、Droid 以外は height
が取れるけど Droid だけは primaryFunction
が取れるので、取得したデータの構造はデータによって異なる
Input types
スカラー型や列挙型以外にも複雑なオブジェクトをそのまま渡すこともできる。
これはミューテーションの時に便利で、更新したい値そのものを渡すことができる。
input ReviewInput {
starts: Int!
commentary: String
}
mutation CreateReviewForEpisode($ep: Episode!, $review: ReviewInput!) {
createReview(episode: $ep, review: $review) {
stars
commenary
}
}
↓
{
"ep": "JEDI",
"review": {
"stars": 5,
"commentary": "This is a great movie!"
}
}
これで、レビューが送信されて登録されつつ、 stars
と commentary
がレスポンスでも返ってくる
Resolvers
resolver
(リゾルバ)とは、クエリで指定された時に実行される関数のこと
DB にアクセスして値を返すための関数
resolver の説明の例として以下のようなスキーマがあるとする
type Query {
human(id: ID!): Human
}
type Human {
name: String
appearsIn: [Episode]
starships: [Starship]
}
enum Episode {
NEWHOPE
EMPIRE
JEDI
}
type Starship {
name: String
}
resolver はこんな感じで用意する
Query: {
human(obj, args, context, info) {
// human がクエリで要求された時に動く処理。DBとかから必要な情報を取得して返す
return context.db.loadHumanByID(args.id).then(
userData => new Human(userData)
)
}
}
resolver は引数を4つ受け取る
-
obj
- 親 resolver から受け取ったオブジェクト(prarent)
-
args
- このフィールドに渡された引数
-
context
- GraphQL の resolver 全体で共有されるオブジェクト
- dbのセットアップとか、認証情報をこれで共有して使いまわせる
- サーバーの設定のタイミングで中身を設定しておける
- https://graphql.org/graphql-js/express-graphql/#graphqlhttp
-
info
- 実行したオペレーションに関する詳細情報。通常は使わないらしい
Apollo
そろそろ基礎がわかってきたので実際に使うことになるであろう Apollo を学んでみたい。
何ができるのか
他にもありますが大体使うのはこの3つかなと思います
- Apollo Server で GraphQL サーバーを簡単に立ち上げることができる
- Apollo Client でクライアントからクエリを叩く
- Apollo Studio Explorer でブラウザから操作ができるGUIが使える
参考: https://www.apollographql.com/docs/
Apollo Server
使い方は 公式 の通りに入れればOKみたい
セットアップは簡単で、セットアップに必要な schema と resolver をいい感じに作って読み込ませれば良いだけ
導入方法をググると apollo-server-micro の記事しか出てこないけど、@apollo/server に変えたほうがいいらしい
(追記)↓ 全然参考記事がなくてだいぶ詰まっちゃったので記事にしました
Next.js の API Routes で @apollo/server の4系を動かす例
最新が4系みたいだけど Next.js の API Routes での動かしたかったけど記事が全然出てこなかった
Next.js の公式では GraphQL Yoga を使ったサンプル があったのでそれと GraphQL の公式に書いてあった Express を使う方式で入れてみたけどうまくいかず...
色々探していたら @apollo/server の4系にしたい人はみんな困ってたらしく @as-integrations/next というライブラリがあったので、これを使ってみたら動いた!(調べまくって丸一日潰した...)
使い方もライブラリの startServerAndCreateNextHandler()
関数をかませばいいだけなので使用感はほぼ変わらずに使えるっぽい
これでページ側は http://localhost:3000/ で閲覧できて、http://localhost:3000/api/graphql で GraphQL サーバーにリクエストが送信できる(ブラウザで叩けば Apollo Studio Explorer が起動する)
import { ApolloServer } from '@apollo/server';
import { startServerAndCreateNextHandler } from '@as-integrations/next';
const resolvers = {
Query: {
hello: () => 'world',
},
};
const typeDefs = gql`
type Query {
hello: String
}
`;
const server = new ApolloServer({
typeDefs,
resolvers,
});
export default startServerAndCreateNextHandler(server);
Apollo Client
Next.js で使いたいので 公式の React の Get Started を参考に進めていく
Next.js で @apollo/client を使う例
準備は ApolloClient
を new
するだけで良い
全部のページで使いたいので ApolloProvider
で囲む
import 'styles/globals.css'
import type { AppProps } from 'next/app'
import { ApolloProvider, ApolloClient, InMemoryCache } from '@apollo/client';
export default function App({ Component, pageProps }: AppProps) {
const client = new ApolloClient({
uri: '/api/graphql',
cache: new InMemoryCache(),
});
return (
<ApolloProvider client={client}>
<Component {...pageProps} />
</ApolloProvider>
);
}
↓ ページ側は userQuery
にクエリ文字列を渡して実行する
import { gql, useQuery } from '@apollo/client';
const GET_USER = gql`
query getUser($id: ID!) {
user(id: $id) {
name
}
}
`
const User = () => {
const { data, loading, error } = useQuery(GET_USER, { variables: { id: '00001' } });
// 以下省略
}
GraphQL Code Generator
これが結構目玉っぽい
GraphQL を実装する上でこれがあると開発体験がかなり良くなるみたいなので使いたい。
何ができるのか
今回やろうとしているのは以下の2つ
これらがあるとバックエンド側の resolver の開発だったり、フロント側でクエリを叩くのが楽になったりするはず。
- スキーマから TypeScript の型定義を自動生成する
- React hooks を生成する(クエリごとの
userQuery
関数を作ってくれる)
他にもプラグインとか使って色々できたりとても多機能っぽい
使い方
codegen.yml
というファイルを作ってそこに設定を書いていく
overwrite: true # 生成時にファイルを上書きして良いか
schema: "./src/graphql/schema.graphql" # スキーマファイルへのパスを指定
documents: "./src/graphql/client/**/*.graphql" # フロント側で使うクエリのファイルを読み込む
generates:
src/types/generated/serverGraphql.d.ts: # 型定義を作るためのパスとプラグインを指定
plugins:
- "typescript"
- "typescript-resolvers"
src/types/generated/clientGraphql.tsx: # React hooks を生成するためのパスとプラグインを指定
plugins:
- "typescript"
- "typescript-operations"
- "typescript-react-apollo"
moga さんのテンプレート を参考にしました
あとは実行するための script を package.json
に記載して
"codegen": "graphql-codegen --config codegen.yml"
↓実行するとレシピ通りにファイルが生成される
npm run codegen
書き出し先とか、書き出す際の設定とか、もっとたくさんやれることがあると思うのでそこは今後調査したい。
一旦これで型定義とクライアントで叩くための hooks が作られたので最低限使えるようになりました!
Next.js + Apollo + GraphQL Code Generator のいい感じの構成を考える
最低限使えるようになったので Next.js でのいい感じの構成を考える。
ディレクトリ構造
関係のないファイルは省略
myapp
│
├─ src/
│ │
│ ├─ graphql/ # GraphQL の関連ファイルはここにまとめる
│ │ │
│ │ ├─ client/ # GraphQL のクライアント用のファイル置き場
│ │ │ ├─ mutation/
│ │ │ │ └─ saveUser.graphql # 1ファイル1ミューテーションで増やしていく
│ │ │ ├─ query/
│ │ │ │ └─ user.graphql # 1ファイル1クエリで増やしていく
│ │ │ └─ index.ts # new ApolloClient をする関数を書いておく。 src/pages/_app.ts で使う
│ │ │
│ │ ├─ server/ # GraphQL のサーバー用のファイル置き場
│ │ │ ├─ resolvers/ # リゾルバをいい感じに生やす
│ │ │ │ └─ index.ts
│ │ │ └─ index.ts # new ApolloServer をする関数を書いておく。 src/pages/api
│ │ │
│ │ └─ schema.graphql
│ │
│ ├─ pages/
│ │ │
│ │ ├─ api/
│ │ │ └─ graphql.ts # new ApolloServer を実行してAPIを生やす。`/api/graphql` のエンドポイントになる
│ │ │
│ │ ├─ _app.tsx # ApolloProvider で全ページで使えるように設定
│ │ └─ index.tsx # 各ページでは自動生成された useQuery の hooks を使って GraphQL にリクエストを送信する
│ │
│ └─ types/
│ ├─ clientGraphql.tsx # 自動生成されたクライアント側で使う hooks
│ └─ serverGraphql.d.ts # 自動生成された型定義
│
└─ codegen.yml # GraphQL Code Generator に必要な設定ファイル。これも `src/graphql` の中に入れてもよかったけど設定ファイルがルートにあると何を使ってるか分かりやすくて好きなのでよしとする
Apollo Server
外部のスキーマファイルを読み込みたいので fs
とかを使ってファイルを読み込む
import { readFileSync } from 'fs';
import { resolve } from 'path';
import { ApolloServer } from '@apollo/server';
import { startServerAndCreateNextHandler } from '@as-integrations/next';
import { resolvers } from './resolvers';
export const initializeApolloServer = () => {
const typeDefs = `#graphql
${readFileSync(resolve(process.cwd(), './src/graphql/schema.graphql')).toString()}
`;
const server = new ApolloServer({
typeDefs,
resolvers,
});
return startServerAndCreateNextHandler(server);
}
開発の流れ
やっといい感じに開発ができるところまできたので、ここからの開発は改めて以下の流れになると思う。
スキーマ定義
バックエンドの実装
フロントエンドの実装
スキーマを定義して型定義を自動生成して、Apollo Studio Explorer で挙動を確かめながらバックエンドの resolver を開発して、フロント側でデータ取得のためのクエリを書いて hooks を自動生成して、ページ側で使う。
この流れで試しにやってみる