Open12

GraphQL の基礎を理解し、Apollo や GraphQL Code Generator を使える様になるまでの勉強の流れ

yuyu

何度か勉強したけどいまいちわからずのままなので、勉強したことをここに書き残す。

目標

概念を理解して何か作ってみる
Firebase が好きなので DB は Firestore を使ってみたいけどどうなんだろう

現状

Apollo GraphQL とか GraphQL Code Generator とかを使って Next.js の上に GraphQL サーバーを立ててフロントから使えるようにはできたけど、全く意味が理解できていない!

いきなり大きい構成でやっているのが悪い気がするので小さいサンプルから学んでいく

やること

まずは周辺の単語を理解したい。
初めて出会うワードが結構あるのでそれぞれがどういう意味なのかを理解する。
英語の勉強もかねて自力で翻訳しながら読んでみる

何はともあれ公式ドキュメント
https://graphql.org/learn/

yuyu

いろいろな組み合わせの使い方が用意されている...すごい...
自分は TypeScript + Apollo を見るのが良さそうだけど、一旦公式ドキュメントを先に見てから
https://www.howtographql.com/

yuyu

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"
      }
    ]
  }
}
yuyu

Mutations

データを更新するためのリクエストを定義するもの

  • GETquery
  • POSTmutation
  • PATCHmutation
  • DELETEmutation

みたいな感じ。

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 を作ってサービスを作ろうとしてた時に、更新後にまたデータとってくるリクエストしないといけないのどうしようかなと思ってたので、それが一発でいい感じになるよって感じかな

yuyu

概念の話

GraphQL はクエリ言語と呼ばれている。
エンドポイントが一つだけで、リクエスト時に投げるクエリによって取得するデータの構造などを決めることができる。

REST API みたいに機能やページごとでエンドポイントを切っている場合は特定のAPIにフィールドを追加して欲しい時とかにバックエンド側への依頼が必要になるが、GraphQLならフロントエンド側でクエリを書き換えれば取得できる内容が変わるので特にコミュニケーションが不要になる。

また、REST の場合は使わないデータも毎回取得してしまうことになるが、GraphQL であれば必要な時に必要なデータだけ取得することができる。

実装は大きく3つに分かれる

概念の理解でややこしいのが全体像が見えてこないことな気がしている。
まだ完全にわかったわけではないけど、おそらくこの3つが大きな柱になるところ。

  1. スキーマ定義
  2. バックエンドの実装
  3. フロントエンドの実装

今の理解だとまずはスキーマ定義をして、それをもとにバックエンドの実装とフロントエンドの実装をそれぞれ行う流れになるだと思う。
なんか全部いい感じになるんでしょ?ってことはなくて、それぞれちゃんと定義や実装が必要なはず

Apollo とか、GraphQL Code Generator を使えば、サーバーの立ち上げが簡単だったり、スキーマからコードが自動で生成されるようにできたりして開発が楽にできるみたいなイメージ(どこまで自動化できるんだろうか)

理解が難しい点としては上記のような便利なツールがたくさんあって、それらのどれかを使って実装するのがスタンダードになっているので、GraphQL 自体が何を持っていて何を指しているのか、Apollo は何をしてくれるのかとかが分かりづらくなっているのかなと思った。

個人的にも Apollo で動かすところまでは簡単にいったけど、よくわからなくて一番シンプルな採用単位の GraphQL のサンプルを読んでいるところ

yuyu

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!"
yuyu

型について

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!"
  }
}

これで、レビューが送信されて登録されつつ、 starscommentary がレスポンスでも返ってくる

yuyu

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
  • info
    • 実行したオペレーションに関する詳細情報。通常は使わないらしい
yuyu

Apollo

そろそろ基礎がわかってきたので実際に使うことになるであろう Apollo を学んでみたい。
https://www.apollographql.com/

何ができるのか

他にもありますが大体使うのはこの3つかなと思います

参考: https://www.apollographql.com/docs/

Apollo Server

使い方は 公式 の通りに入れればOKみたい
セットアップは簡単で、セットアップに必要な schema と resolver をいい感じに作って読み込ませれば良いだけ

導入方法をググると apollo-server-micro の記事しか出てこないけど、@apollo/server に変えたほうがいいらしい

(追記)↓ 全然参考記事がなくてだいぶ詰まっちゃったので記事にしました
https://zenn.dev/yu_undefined/articles/8cdc18028b908d

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 が起動する)

src/pages/api/graphql.ts
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);

https://itc-engineering-blog.netlify.app/blogs/nextjs-graphql-bff-apollo-server
https://stackoverflow.com/questions/74865910/upgrading-from-apollo-server-micro-to-apollo-server-4

Apollo Client

Next.js で使いたいので 公式の React の Get Started を参考に進めていく

Next.js で @apollo/client を使う例

準備は ApolloClientnew するだけで良い
全部のページで使いたいので ApolloProvider で囲む

src/pages/_app.tsx
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 にクエリ文字列を渡して実行する

src/pages/index.tsx
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' } });
  // 以下省略
}
yuyu

GraphQL Code Generator

これが結構目玉っぽい
GraphQL を実装する上でこれがあると開発体験がかなり良くなるみたいなので使いたい。
https://the-guild.dev/graphql/codegen

何ができるのか

今回やろうとしているのは以下の2つ
これらがあるとバックエンド側の resolver の開発だったり、フロント側でクエリを叩くのが楽になったりするはず。

  • スキーマから TypeScript の型定義を自動生成する
  • React hooks を生成する(クエリごとの userQuery 関数を作ってくれる)

他にもプラグインとか使って色々できたりとても多機能っぽい

使い方

codegen.yml というファイルを作ってそこに設定を書いていく

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 が作られたので最低限使えるようになりました!

yuyu

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 とかを使ってファイルを読み込む

src/graphql/server/index.ts
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);
}
yuyu

開発の流れ

やっといい感じに開発ができるところまできたので、ここからの開発は改めて以下の流れになると思う。

  1. スキーマ定義
  2. バックエンドの実装
  3. フロントエンドの実装

スキーマを定義して型定義を自動生成して、Apollo Studio Explorer で挙動を確かめながらバックエンドの resolver を開発して、フロント側でデータ取得のためのクエリを書いて hooks を自動生成して、ページ側で使う。

この流れで試しにやってみる