本文書は今後GraphQLを利用する機会があるかもしれないのでGraphQLを一通り一度に学習したいというGraphQLの入門者の方に向けて作成したチュートリアルです。

GraphQLのサーバの構築(Apollo Server)、REST APIを使って外部リソースからのデータ取得(JSONPLACEHolder)、データベースの接続(Prisma)、クライアント(React, Vue)からのデータの取得まで実際に手を動かしながら動作確認を行なっていくのでGraphQLの全体像を掴むのに必要な基本的な知識を習得することができます。

GraphQLの本質とは離れた場所でできるだけ悩まないようにシンプルな構成で動作確認を行なっています。

GraphQLとは

GraphQLという名前はGraphとQLに分けることができます。GraphはGraphQLであつかうデータが以下の図のようにnodeとedgeを使ってグラフのように表されるところから来ています。アプリケーション内のデータはオブジェクトから構成され、オブジェクト間はリレーションを持っています。下記の図のTrackオブジェクトはauthor idを通してAuthoerオブジェクトと繋がっています。オブジェクトをnode、リレーションショップをedgeとして描くと下記のようなグラフとして考えることができます。

データグラフ
データグラフ

QLはQuery Language(クエリー言語)の略でクエリーと言えばデータベースを思い浮かべる人も多いのではないでしょうか。しかしSQLのようにデータベースをクエリーで操作するための言語ではなくAPIのためのクエリー言語でデータベースとは関係がありません。GraphとQLを合わせたGraphQL自体はREST APIの代替となる規格です。

REST APIはクライアントからサーバに対してCRUD(Create, Delete, Update, Delete)する際に利用するようにGraphQLも同様にクライアントからサーバに対してデータをCRUDする際に利用することができます。GraphQLは代替ということでREST APIを使って比較して説明されることが多く例えばREST APIではデータを取得する際に複数のエンドポイント(例:ユーザ一覧から/users, ブログの記事一覧なら/posts,…)を利用します。GraphQLでは1つのエンドポイントのみ持ちクエリーを設定(問い合わせなのか更新・削除なのか、どのデータが欲しいかを指定)することでサーバから取得することができます。クエリーの設定によって一度のHTTPリクエストで一括でユーザ情報、ブログ記事情報を取得することも可能な上、ユーザ情報の中からはemailだけといったように欲しいデータのみ選択して取得するといったことも可能です。REST APIでは/usersにアクセスしてデータを取得する際emailが欲しいからといってemailのみ取得することはできません。

GraphQLはREST APIがかかえる2つの問題を解決することを目的に開発されています。一つはUnderfetchingで必要なデータを取得する際に1つのエンドポイントから十分なデータが取得できない場合に複数のエンドポイントに対してリクエストを送信しなければならないことです。もう一つはOverfetchingと呼ばれ先ほど説明しましたがユーザデータが欲しい場合emailのみ取得するといったことができず必ず一緒に必要でない情報を取得しなければならないことです。

言葉で説明するよりも実際に動作確認を行うとGraphQLがどのようなものかすぐに納得できるかと思います。現時点でGraphQLのイメージがわからない人も本文書を読み終えた時はGraphQLドキュメントに記載されている下記の説明が理解できるようになるかと思います。

“GraphQL is a new API standard that provides a more efficient, powerful and flexible alternative to REST.At its core, GraphQL enables declarative data fetching where a client can specify exactly what data it needs from an API. Instead of multiple endpoints that return fixed data structures, a GraphQL server only exposes a single endpoint and responds with precisely the data a client asked for.”

GraphQLサーバの構築

GraphQLというプロダクトが存在するわけではないのでGraphQLサーバを構築する方法はいくつかあります。本文書ではGraphQLサーバの構築はApollo Serverを利用して行います。ここからの構築作業では動作確認環境にNodeがインストールされている必要があります。もし入っていない場合はインストールを行っておく必要があります。

Apollo Serverの構築

任意の名前のプロジェクトフォルダgraphql-apollo-serverを作成します。作成後、作成したフォルダに移動します。npm init -yコマンドでpackage.jsonファイルを作成します。


 % mkdir graphql-apollo-server
 % cd graphql-apollo-server 
 % npm init -y

Apollo Severにはapollo-serverとgraphqlのライブラリをインストールする必要があります。graphqlはGraphQLのスキーマとスキーマに対してクエリーを実行するために利用するライブラリです。graphqlはクライアントでもインストールします。


 % npm install apollo-server graphql

index.jsファイルを作成します。


 % touch index.js

index.jsファイルの更新を検知でき更新した内容が自動反映できるようにnodemonもインストールを行っておきます。インストール後にnpx nodemon index.jsコマンドを実行すると監視が開始されます。


 % npm install --save-dev nodemon 
 % npx nodemon index.js
[nodemon] 2.0.15
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): *.*
[nodemon] watching extensions: js,mjs,json
[nodemon] starting `node index.js`
[nodemon] clean exit - waiting for changes before restart

作成したindex.jsファイルにGraphQLサーバの設定を記述していきます。index.jsファイルを開いて、apollo-serverからApollo Serverの設定に必要なモジュールApolloServerをrequireします。gqlはスキーマを定義する際に利用します。


const { ApolloServer, gql } = require('apollo-server');

動作確認としてApollo Serverに対してhelloというクエリーを実行すると”Hello World”という文字列を戻す設定を行なっていきます。

schema(スキーマ)の定義

GraphQLではスキーマを使って型の定義を行う必要があります。GraphQLは独自のスキーマ定義言語(SDL: Schema Definition Language)を持っているのでSDLを使って型を定義していきます。クライアントがアクセスするためのデータの定義を行うだけではなく問い合わせ(Query)、更新、削除(Mutation)などにも型定義を行なっていきます。

スキーマでは主にクライアントがアクセスを行うオブジェクトタイプのスキーマを定義していきます。オブジェクトタイプはGraphQLを利用したことがなくてもイメージが湧きやすいタイプです。ユーザに関するオブジェクトタイプであれば下記のような形を持ちます。各フィールドにはフィールド名と型を設定するだけです。


type User {
  id: ID!
  name: String!
  email: String!
}

スキーマタイプにはオブジェクトタイプの他にQueryタイプ、Mutationタイプ、Subscriptionタイプがあります。

QueryタイプはRead Operation(問い合わせ)を行うクエリーの定義に利用します。MutationタイプはWrite Operations(作成、更新、削除)に利用します。SubscriptionタイプはChatアプリなどのようなリアルタイムのRead Operationに利用します。Subscriptionは本文書では扱いません。

Queryタイプについてはクライアントが実行するクエリーを定義していきます。フィールド毎に名前とクエリーの実行後に戻される型を設定します。

では実際に動作確認を行うhelloクエリーをQueryタイプに追加していきましょう。スキーマはgql関数の中に記述していきます。Queryタイプの中でhelloのクエリーが実行されるとStingのタイプが戻されるということを定義しています。Queryタイプの中に実際のクエリーの処理を記述することはありません。他のクエリーを追加したい場合はQueryタイプの中に追加していくことになります。


const typeDefs = gql`
  type Query{
    hello: String
  }
`

resolvers(リゾルバ)の設定

スキーマの中でアプリケーション内で扱うデータがどのようなフィールドを持ち、そのフィールドがどのような型を持っているのか定義することがわかりました。しかしどのように定義した型通りのデータを設定するかが記述されていません。helloをQueryタイプに追加し、helloはStringの型を戻すことはわかりましたがどのような文字列が戻されるのかもわかりません。Queryタイプで定義した戻り値の型通りにどのようにデータを設定するのかを記述するのがresolver(リゾルバ)です。リゾルバは関数を使って記述します。

Queryタイプに記述したhelloクエリーは戻り値がStringになることが定義されています。そのためリゾルバでは関数を使って以下のように記述することができます。戻り値はhelloクエリーで定義したStringになっています。もし文字列を戻さない場合には型が異なるためエラーになります。リゾルバの中ではスキーマの定義を元にデータ処理を記述していきます。


const resolvers = {
  Query: {
    hello: () => 'Hello World',
  },
}

サーバの起動

GraphQLサーバの起動に作成したスキーマとリゾルバを設定します。


const server = new ApolloServer({ typeDefs, resolvers });

server.listen().then(({ url }) => {
  console.log(`🚀  Server ready at ${url}`);
});

ここまでの全体のコードの記述は下記の通りです。クライアントからのクエリーからデータを戻すことができる最もシンプルなGraphQLサーバです。


const { ApolloServer, gql } = require('apollo-server');

const typeDefs = gql`
  type Query{
    hello: String
  }
`

const resolvers = {
  Query: {
    hello: () => 'Hello World',
  },
}

const server = new ApolloServer({ typeDefs, resolvers });

server.listen().then(({ url }) => {
  console.log(`🚀  Server ready at ${url}`);
});

ここまでの設定で本当に”Hello World”が戻されるのか確認したいと思います。nodemonを使ってindex.jsファイルを記述していくとnodemonを実行したコンソールにGraphQLサーバのURLが表示されます。


[nodemon] starting `node index.js`
🚀  Server ready at http://localhost:4000/

ブラウザでhttp://localhost:4000にアクセスすると下記の画面が表示されます。”Query your server”ボタンをクリックしてください。

Apollo Serverの初期画面
Apollo Serverの初期画面

Apollo Studioが起動します。クラウドベースのツールでApollo Studio Explorerを利用することでブラウザ上からクエリーを実行し動作確認を行うことができます。画面の左側を見るとindex.jsで設定したhelloを確認することができます。

Apollo Studioの画面
Apollo Studioの画面

Operationsを空にした状態から左側のドキュメンテーションのQueryとFieldsの下にあるhelloをクリックしてください。Operationsにクエリーが表示されます。これがGraphQLに送信するクエリーの中身です。右上の”Query”ボタンをクリックしてください。

初めてのクエリー
初めてのクエリー

Responseに”Hello World”が表示されます。GraphQLを対してクエリーを実行し、Queryタイプで設定した型で、リゾルバで設定した文字列が戻されることが確認できました。

クエリーを実行
クエリーを実行

ここまでの動作確認でGraphQLでは”スキーマ”と”リゾルバ”というものが重要な構成要素だということが理解できました。

さらにhelloを使って動作確認を行います。helloクエリーに引数を設定することもできます。クエリーに引数を設定する場合は引数にも型の設定を行う必要があります。nameという引数の型をStringに設定しています。文字以外の数字を入れるとエラーになります。


const typeDefs = gql`
  type Query {
    hello(name: String): String
  }
`;

引数を設定後、Apollo Studio Explorerでクエリーを実行しても戻される値にも変化はありません。次に必須を表わす”!”を引数のタイプのStringの後に追加します。


const typeDefs = gql`
  type Query {
    hello(name: String!): String
  }
`;

引数が必須になったことでApollo StudioのOperationsの中にメッセージが表示されるようになります。型の設定によって間違いがある場合にすぐに気がつくことができます。

必須メッセージが表示
必須メッセージが表示

Queryタイプで引数を設定した場合にはリゾルバでも引数が取得できるように設定を行う必要があります。リゾルバの関数にはparent, args, context, infoの4つ引数を受け取ります。各引数については今後利用する場合に説明をしていきます。ここで知りたいのはQueryタイプの引数で設定したnameの値です。argsはargumentの略で引数を持っているのでargsにどのように値が入るか確認を行います。


const resolvers = {
  Query: {
    hello: (parent, args) => {
      console.log('parent', parent);
      console.log('args', args);
      return 'Hello World';
    },
  },
};

Apollo Studio上ではクエリーは下記のように引数を設定することができます。

Apollo Studioで引数を設定
Apollo Studioで引数を設定

”Query”ボタンをクリックするとGraphQLサーバを起動しているコンソールにオブジェクトとして”John Doe”が入っていることがわかります。parentは”undefined”です。parentについては後ほど出てくるのでここで”undefined”になっていることを覚えていてください。


parent undefined
args { name: 'John Doe' }

argsにオブジェクトとして引数が入っていることがわかったのでnameの値を取得するために下記のように更新することができます。


const resolvers = {
  Query: {
    hello: (parent, args) => `Hello ${args.name}`,
  },
};

再度クエリーを実行するとResponseに引数に設定した”John Doe”が表示されます。引数の設定方法を理解することができました。

引数の表示
引数の表示

スキーマの追加(User)

Queryタイプではhelloクエリーを定義しましたが次はオブジェクトタイプのスキーマを定義する方法を確認していきます。オブジェクトタイプには特別なタイプであるQuery、Mutation、Subscriptionという名前はつけることができませんがそれ以外の任意の名前をつけることができます。

オブジェクトタイプは通常複数のフィールドで構成されます。Userタイプがidとnameとemailの3つのフィールドで構成されている場合下記のように記述することができます。各フィールドにもそれぞれ型の設定を行います。フィールドタイプにはスカラータイプとオブジェクトタイプがあります。スカラータイプにはString, Int, ID, Booleanなどがあります。idにはID型を設定しています。Stringと同じ文字列ですが一意である必要があります。nameとemailにはString型を設定しています。”!”がついているのでどのフィールドも値が必須です。オブジェクトタイプは定義済みのオブジェクトタイプを型として利用する場合に使用します。後ほど別のオブジェクトタイプを追加した時に確認します。


const typeDefs = gql`
  type User {
    id: ID!
    name: String!
    email: String!
  }

  type Query {
    hello(name: String!): String
  }
`;

スキーマで定義したUserタイプの型を元に実際に利用するユーザ情報のデータを作成します。


const users = [
  { id: '1', name: 'John Doe', email: 'john@test.com' },
  { id: '2', name: 'Jane Doe', email: 'jane@example.com' },
];

クライアントからクエリーを使ってユーザ情報を取得できるようにQueryタイプのフィールドにクエリーの名前と戻り値の型を設定する必要があります。戻り値については空の配列か先ほど定義したオブジェクトタイプUserが配列が入るため[User]と記述します。usersのクエリーを実行するとオブジェクトタイプのUserの型を持つ配列データが戻されることを表しています。


const typeDefs = gql`
  type User {
    id: ID!
    name: String!
    email: String!
  }

  type Query {
    hello(name: String!): String
    users: [User]
  }
`;

Queryタイプの設定に基づいて実際のデータを取得するための処理はリゾルバの設定で行います。追加したusersの関数では作成したユーザ情報usersのデータを戻す処理を行なっています。


const resolvers = {
  Query: {
    hello: (parent, args) => `Hello ${args.name}`,
    users: () => users,
  },
};

Apollo Studioを利用してusersクエリーを実行します。先ほどまでのhelloとは異なり左側のDocumentationにスキーマの中で定義したUserタイプのフィールド名とその型が表示されます。クエリーではUserを構成するフィールドを選択して指定することができます。”Query”ボタンをクリックするとReponseにユーザの情報が表示されます。

usersに対するクエリー
usersに対するクエリー

もしemailだけ情報が欲しい場合は下記のようにフィールドemailだけ指定することでemailのみ取得することができます。

フィールドを選択
フィールドを選択
GraphQLではフィールドをクライアント側で選択できるのがREST APIと大きく異なる特徴の一つです。

あるidを持つユーザのみ取得したい場合にもQueryタイプとリゾルバに追加を行います。userクエリーでは引数を利用するためにargsを利用しています。


const typeDefs = gql`
  type User {
    id: ID!
    name: String!
    email: String!
  }

  type Query {
    hello(name: String!): String
    users: [User]
    user(id: ID!): User
  }
`;

const resolvers = {
  Query: {
    hello: (parent, args) => `Hello ${args.name}`,
    users: () => users,
    user: (parent, args) => {
      const user = users.find((user) => user.id === args.id);
      return user;
    },
  },
};

設定後Apollo Studioを利用してクエリーを実行するとidに指定したユーザのみ結果に表示されます。型のIDは文字列なので渡す変数も文字列にしておく必要があります。

idに2を持つユーザ情報のみ表示
idに2を持つユーザ情報のみ表示

外部データソースからのデータ取得

ユーザ情報をindex.jsの中に直接記述しましたが外部のデータソースを利用してデータを取得する方法を確認していきます。GraphQLサーバがクライアントからのクエリーを受け取り、そのクエリーの内容を元に外部のデータリソースにアクセスを行いデータを取得して指定された情報のみクライアントに戻すという流れになります。

外部データリソースには無料で利用可能なJSONPlaceHolderを利用します。決められたURLにGETリクエストを送信するとJSONでデータを取得することができます。

データを取得するためにaxiosのライブラリを利用するためaxiosライブラリのインストールを行います。fetch関数を利用するためにnode-fetchをインストールすることもできます。


% npm install axios

ユーザ情報一覧を取得するためhttps://jsonplaceholder.typicode.com/usersにアクセスを行います。

データリソースの場所が変更になるだけなので戻り値の変更はないのでスキーマのQueryタイプの更新はありません。リソルバのusers関数でのデータの取得方法を変更します。データの取得には非同期関数のasync, awaitを利用しています。


const resolvers = {
  Query: {
    hello: (parent, args) => `Hello ${args.name}`,
    users: async () => {
      const response = await axios.get(
        'https://jsonplaceholder.typicode.com/users'
      );
      return response.data;
    },
    user: (parent, args) => {
      const user = users.find((user) => user.id === args.id);
      return user;
    },
  },
};

リゾルバを更新後、Apollo Studioでクエリーを実行するとJSONPlacehoderから10名分のユーザ情報を取得することができます。

JSONPLACEHollderから取得したデータを表示
JSONPlaceholderから取得したデータを表示

リゾルバのuser関数の変更も行います。個別のユーザの情報を取得したい場合にはURLはhttps://jsonplaceholder.typicode.com/users/{id}でidを変更することでidが一致するユーザのみ情報を取得することができるので下記のように更新することができます。


const resolvers = {
  Query: {
    hello: (parent, args) => `Hello ${args.name}`,
    users: async () => {
      const response = await axios.get(
        'https://jsonplaceholder.typicode.com/users'
      );
      return response.data;
    },
    user: async (parent, args) => {
      const response = await axios.get(
        `https://jsonplaceholder.typicode.com/users/${args.id}`
      );
      return response.data;
    },
  },
};;

クライアント側からはどこからデータを取得していることは意識することなくGraphQLサーバからユーザ情報を取得することができます。取得する際にGraphQLはエンドポイントはいつも同じなのでクライアント側の変更は必要ありません。

スキーマの追加(POST)

スキーマに新たにオブジェクトタイプのPOSTを追加します。

POSTはuserId, id, title, bodyの4つのフィールドを持っておりuserIdはUserのidが入ります。UserとPOSTはリレーションを持っているのでUserのmyPostsフィールドにオブジェクトタイプのPostを設定します。


const typeDefs = gql`
  type User {
    id: ID!
    name: String!
    email: String!
    myPosts: [Post]
  }

  type Post {
    id: ID!
    title: String!
    body: String!
    userId: ID!
  }

  type Query {
    hello(name: String!): String
    users: [User]
    user(id: ID!): User
  }
`;

Queryタイプにフィールドpostsを追加して記事一覧の情報が取得できるようにresolversにも追加したフィールドpostsとその関数を追加します。記事一覧もJSONPlaceholderから取得しURLはhttps://jsonplaceholder.typicode.com/postsです。


type Query {
  hello(name: String!): String
  users: [User]
  user(id: ID!): User
  posts: [Post]
}
//略
const resolvers = {
  Query: {
//略
    posts: async () => {
      const response = await axios.get(
        'https://jsonplaceholder.typicode.com/posts'
      );
      return response.data;
    },
  },

Apollo Studioからクエリーを実行すると100件分の記事一覧が表示されます。

記事一覧を取得
記事一覧を取得

posts一覧の取得はusersの取得と同じなので問題はないと思います。ではUserに追加したフィールドmyPostsはどのように取得するのか疑問に思う人もいるかと思います。

myPostsをUserタイプに追加したことでApollo Studio上では下記のようなクエリーを作成することができます。クエリーが作成できるのでGraphQLサーバが自動でスキーマのリレーションを解釈してpostsの情報を取得できるのではと思った人もいるかもしれませんが残念ながらそうようなことは起こらず”User”ボタンをクリックしてクエリーを実行するとResponseを見るとユーザ情報は表示されますがmyPostsの値はnullです。

userクエリーからmyPostsを指定
userクエリーからmyPostsを指定

ではどのようにすればmyPostsに記事情報が保存されるのでしょう。リゾルバにmyPostsを取得するための処理を追加する必要があります。

更新した関数の中では最初にidを利用してユーザ情報を取得します。JSONPlaceholderにはuserのidを元に一括で記事一覧を取得するURLは提供されていないので一度すべての記事一覧を取得します。取得した記事一覧の中からfilter関数を利用してuserのidが一致する記事のみ取得します。最後にObject.assignを利用して取得したデータをマージしてそれを戻しています。


user: async (parent, args) => {
  let response = await axios.get(
    `https://jsonplaceholder.typicode.com/users/${args.id}`
  );
  let user = response.data;
  response = await axios.get('https://jsonplaceholder.typicode.com/posts');
  const myPosts = response.data.filter((post) => post.userId == args.id);
  user = Object.assign({}, user, {
    myPosts: myPosts,
  });
  return user;
},

再度Apollo Studioで先ほどmyPostsがnullであったクエリーを実行すると今度はmyPostsが含まれた情報を取得することができます。

userと一緒にpostsも取得
userと一緒にpostsも取得

指定したユーザのidが持つ記事のタイトルのみ表示したい場合は下記のように実行することでタイトルのみ取得することができます。

タイトルのみ取得
タイトルのみ取得

このようにスキーマで定義したタイプになるようにリゾルバの関数の中で処理を記述していく必要があります。

parentの利用したリゾルバ設定

ユーザに紐づいた記事一覧の取得のクエリーをApollo StudioのOperationsを見るとuser→myPosts→titleという風に階層になっていることがわかります。

クエリーの階層
クエリーの階層

上記の階層から順番に処理が行われることになります。リゾルバの関数の引数にはparent, args, context, infoというものがあったことを思い出してください。helloクエリーではparentの値はundefinedでした。これは上記の階層を見るとhelloに対応するuserの上には親(parent)が存在しないためです。myPostsから見るとuserが親に当たりparentを利用することで親であるuser関数で実行した内容を取得することができます。

myPostsを別の関数として定義するためにリゾルバにUserを追加してその中にmyPostsの関数を追加します。Queryの中ではありません。


const resolvers = {
  Query: {
//略
  },
  User: {
    myPosts: (parent) => {
      console.log(parent);
    },
  },
};

Apollo Studioでクエリーを実行するとusers関数で実行されて取得したデータがGraphQLサーバを起動したコンソールに表示されます。つまり親からusers関数で実行したデータを受け取ることができるのです。

その中には記事の情報も含まれているのでuserの関数を更新します。ユーザ情報のみ渡せるようにします。


user: async (parent, args) => {
  let response = await axios.get(
    `https://jsonplaceholder.typicode.com/users/${args.id}`
  );
  return response.data;
},

再度Apollo Studioでクエリーを実行するとコンソールにユーザ情報のみ表示されます。

受け取ったparentのデータを利用してそのデータに含まれるユーザidが含まれる記事のみ取得するコードに更新します。


User: {
  myPosts: async (parent) => {
    const response = await axios.get(
      'https://jsonplaceholder.typicode.com/posts'
    );
    const myPosts = response.data.filter((post) => post.userId == parent.id);
    return myPosts;
  },
},

再度Apollo Studioを実行するとmyPostsを分ける前と同じ結果になりました。

タイトルのみ取得
タイトルのみ取得

myPostsを分ける前はusersの中でpostsの値を設定するためにObject.assignを利用してデータの形を調整する必要がりましたがその処理はGraphQLサーバが行ってくれるため追加処理が必要がなくなりました。またクライアントからのクエリーによってはmyPostsのフィールドが必要ない場合にはmyPostsが実行されません。userにすべての処理が含まれる場合はmyPostsの中のフィールドが必要ない場合も取得するための処理は実行されることになります。パフォーマンスとコードのクリーンさを考えるとmyPostsを分けたほうがいいことがわかります。

リゾルバの関数の引数のparentを利用して親からデータを受け取れることがわかりました。

Data sourcesの設定

axiosライブラリを利用してJSONPlaceholderからデータを取得していましたがApollo ServerはREST API用のデータソースライブラリが提供されており、利用することでデータのキャッシュ等を自動で行う機能やfetchの途中でinterceptすることでfetchのheaderに情報を追加することもできます。これは利用が必須の機能ではありませんが動作確認を行います。

REST用のデータソースを利用するためにはapollo-datasource-restパッケージのインストールが必要となります。REST API以外にデータベース用のパッケージも提供されています。


 % npm install apollo-datasource-rest

リゾルバで行っていたaxiosによるデータ取得のコードをデータソースで行えるように変更します。インストールしたapollo-datasource-restパッケージからRESDATASourceを読み込みます。


const { RESTDataSource } = require('apollo-datasource-rest');

RESTDataSourceを継承したjsonPlaceAPIクラスを作成します。任意の名前をつけてください。RESTDataSourceではgetメソッドを利用することができるためaxiosは必要ありません。中身はnode-fetchです。コンストラクターにはsuperが必須でbaseURLを設定します。これはREST APIでデータを取得するサービスのbaseURLです。

getUsers, getUser, getPostsの3つの関数を追加しています。


class jsonPlaceAPI extends RESTDataSource {
  constructor() {
    super();
    this.baseURL = 'https://jsonplaceholder.typicode.com/';
  }

  async getUsers() {
    const data = await this.get('/users');
    return data;
  }
  async getUser(id) {
    const data = await this.get(`/users/${id}`);
    return data;
  }
  async getPosts() {
    const data = await this.get('/posts');
    return data;
  }
}

作成したjsonPlaceAPIはApolloServerの引数に設定を行う必要があります。このように設定することでdataSourcesをリゾルバの中でcontextから取得できるようになります。


const server = new ApolloServer({
  typeDefs,
  resolvers,
  dataSources: () => {
    return {
      jsonPlaceAPI: new jsonPlaceAPI(),
    };
  },
});

リゾルバの中でデータソースで追加した関数を利用することができますがdataSourcesについてはリゾルバの3番目の引数であるcontextからアクセスすることができます。


Query: {
  hello: (parent, args) => `Hello ${args.name}`,
  users: async (parent, args, { dataSources }) => {
  },
  user: async (parent, args, { dataSources }) => {
    return dataSources.jsonPlaceAPI.getUser(args.id);
  },
  posts: async (parent, args, { dataSources }) => {
  },
},
User: {
  myPosts: async (parent, args, { dataSources }) => {
    const posts = await dataSources.jsonPlaceAPI.getPosts();
    const myPosts = posts.filter((post) => post.userId == parent.id);
    return myPosts;
  },
},

parentやargsなど利用しない場合は慣例でparentは_, argsは__と記述します。


Query: {
  hello: (_, args) => `Hello ${args.name}`,
  users: async (_, __, { dataSources }) => {
    return dataSources.jsonPlaceAPI.getUsers();
  },
  user: async (_, args, { dataSources }) => {
    return dataSources.jsonPlaceAPI.getUser(args.id);
  },
  posts: async (_, __, { dataSources }) => {
    return dataSources.jsonPlaceAPI.getPosts();
  },
},
User: {
  myPosts: async (parent, __, { dataSources }) => {
    const posts = await dataSources.jsonPlaceAPI.getPosts();
    const myPosts = posts.filter((post) => post.userId == parent.id);
    return myPosts;
  },
},

axiosからデータソースに変更をしてもこれまで通りJSONPLaceHolderからデータを取得することはできます。データソースの設定方法を理解することができました。

データベースへの接続

JSONPlaceHolderはデータの取得はできますがデータの作成、更新、削除の処理を行うことができません。データの作成、更新、削除を行うためにデータベースを利用します。Prismaを利用してSQLiteデータベースを利用します。

Prismaとは

PrismはORM(オブジェクト-リレーショナルマッピング)でPrismを利用することで接続するデータベースの種類を意識する必要がなくなりSQLを記述しなくても共通のオブジェクトメソッドを利用してデータベースの操作を行うことができます。テーブルの作成もスキーマファイルを作成してマイグレートコマンドで行うことができます。テーブル作成時にSQLを記述することはありません。

Prismaを使ったことがない人だと少し不安になるかもしれませんが本文書であつかう範囲では難しい箇所はありません。手順通りに進めていけばデータベースを利用することができます。またTypeScriptも利用しません。

Prismaのインストール

Prismaを利用するためにはライブラリのインストールが必要になります。


 % npm install prisma --save-dev

npx prism initコマンドを実行すると.envファイルとprismaフォルダが作成されその中にshema.prismaファイルが作成されます。


 % npx prisma init

✔ Your Prisma schema was created at prisma/schema.prisma
  You can now open it in your favorite editor.

Next steps:
1. Set the DATABASE_URL in the .env file to point to your existing database. If your database has no tables yet, read https://pris.ly/d/getting-started
2. Set the provider of the datasource block in schema.prisma to match your database: postgresql, mysql, sqlite, sqlserver or mongodb (Preview).
3. Run prisma db pull to turn your database schema into a Prisma schema.
4. Run prisma generate to generate the Prisma Client. You can then start querying your database.

More information in our documentation:
https://pris.ly/d/getting-started

SQLiteデータベースを利用するのでprismaフォルダに空のdev.dbファイルを作成します。作成後に.envファイルの環境変数DATABASE_URLに作成したファイルのパスを設定します。


DATABASE_URL="file:./dev.db"

prismaフォルダのschema.prismaファイルにはデータベースへの接続情報とデータベースのスキーマを記述します。


generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "sqlite"
  url      = env("DATABASE_URL")
}

model User {
  id    Int     @id @default(autoincrement())
  email String  @unique
  name  String?
  posts Post[]
}

model Post {
  id        Int      @id @default(autoincrement())
  title     String
  body   String?
  author    User?    @relation(fields: [authorId], references: [id])
  authorId  Int?
}

schema.prismファイルを更新したらmigrateコマンドを実行してデータベースにテーブルの作成を行います。実行するとmigrationファイルにつける名前を聞かれます。空白にしても進むことができますが任意の名前をつけてください。


 % npx prisma migrate dev
Environment variables loaded from .env
Prisma schema loaded from prisma/schema.prisma
Datasource "db": SQLite database "dev.db" at "file:./dev.db"

✔ Enter a name for the new migration: … 
Applying migration `20220105101749_`

The following migration(s) have been created and applied from new schema changes:

migrations/
  └─ 20220105101749_/
    └─ migration.sql

Your database is now in sync with your schema.

Running generate... (Use --skip-generate to skip the generators)

added 2 packages, and audited 236 packages in 5s

19 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities

✔ Generated Prisma Client (3.7.0 | library) to ./node_modules/@prisma/client in 148ms

実行が完了するとデータベースの作成は完了です。

本文書ではSQLiteへの接続にTablePlusを利用しています。無料の範囲では制限はありますがデータベースをGUI画面で操作することができます。お手軽なツールでおすすめです。

データベースに直接接続するためPrismaを意識する必要はありません。接続する際には作成したdev.dbを指定する必要があります。接続が完了すると下記のようにschema.prismaに記述した定義に沿ってテーブルが作成されます。

TablePlusからのデータベースへの接続
TablePlusからのデータベースへの接続

次にPrisma Clientの作成を行うために以下のコマンドを実行します。実行すると作成したPrisma Clientのimport方法など利用方法のメッセージも表示されます。


 % npx prisma generate
Environment variables loaded from .env
Prisma schema loaded from prisma/schema.prisma

✔ Generated Prisma Client (3.7.0 | library) to ./node_modules/@prisma/client in 589ms
You can now start using Prisma Client in your code. Reference: https://pris.ly/d/client
```
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
```

Apollo Serverからの接続確認

Apollo Serverから接続して情報が取得できるか確認を行うためにTablePlusを利用してユーザデータの追加を行います。

ユーザデータの登録
ユーザデータの登録

index.jsファイルでPrisma Clientを読み込んでPrismaClientのインスタンスを作成します。


const { PrismaClient } = require('@prisma/client');

const prisma = new PrismaClient();

リゾルバではData Sourceを利用してREST API経由でデータを取得していましたがPrismを使ってデータベースからユーザデータを取得します。

prisma.user.findManyでSQLiteデータベースからユーザデータを一括で取得することができます。


const resolvers = {
  Query: {
    users: () => {
      return prisma.user.findMany();
    },
    //略

TablePlusで登録を行なったユーザのJohn Doeの情報が取得できていることが確認できます。またidが1のためuserIdが1を持つ記事一覧も取得することができています。1つはデータベース、2つはREST APIの2つのデータソースから取得できることも確認できました。

データベースから取得したデータを表示
データベースから取得したデータを表示

Mutationによるユーザ作成

データベース内のデータをGraphQLサーバを利用して取得することができたので次はデータベースへの新規ユーザの作成をGraphQLサーバで行います。

これまではクエリーによるread operationsであったためスキーマはQueryタイプを利用していましたがここからは作成、更新、削除を行うのでwite operationsのMutationタイプを利用します。

スキーマにMutationタイプを追加してcreateUserを追加します。引数にはnameとemailと取ります。作成後にはUserが戻されます。フィールに名前を設定して戻り値を設定をするのでQueryタイプと同じです。


const typeDefs = gql`
//略

  type Mutation {
    createUser(name: String!, email: String!): User
  }
`;

Mutationタイプにフィールドを追加して定義を行ったのでcreateUserで行う処理をリゾルバに記述します。


const resolvers = {
  Query: {
//略
  },
  Mutation: {
    createUser: (_, args) => {
      return prisma.user.create({
        data: {
          name: args.name,
          email: args.email,
        },
      });
    },
  },

Apollo Studioを利用してQueryではなくMutationを実行します。Operationsの中でこれまでqueryと表示されていましたがmutationになっていることを確認してください。”Mutation”ボタンを押して実行すると作成したユーザがResponseに表示されます。idが2として作成されていることがわかります。

Mutationによるユーザの追加
Mutationによるユーザの追加

作成したユーザはTablePlusを利用してデータベースから直接確認することができますがusersクエリーを利用してApollo Studioから確認してみましょう。usersクエリーを実行することで追加したユーザを確認することができます。

queryによる追加したユーザの確認
queryによる追加したユーザの確認

Mutationによるデータの作成を行うことができました。

Mutationによるユーザ更新

既存のユーザに対する更新をMutationを使って行います。Mutationではユーザ名の更新を行います。MutationタイプにupdateUserを追加します。戻り値のタイプはUserとします。引数にはidとnameを指定します。idを利用してユーザを取得して取得したユーザの名前を更新します。


type Mutation {
  createUser(name: String!, email: String!): User
  updateUser(id: Int!, name: String!): User
}

リゾルバでMutationタイプに追加したupdateUserの関数を追加します。


Mutation: {
  createUser: (_, args) => {
    return prisma.user.create({
      data: {
        name: args.name,
        email: args.email,
      },
    });
  },
  updateUser:(_, args) => {
    return prisma.user.update({
      where: {
        id: args.id,
      },
      data: {
        name: args.name',
      },
    });
  },
},

Apollo Studioを利用して追加したMutationのupdateUserを実行します。idを1のユーザの名前の先頭のアルファベットが小文字だったので大文字にしています。更新を行う場合は引数の設定も必要になります。

Mutationでユーザ情報の更新
Mutationでユーザ情報の更新

Mutationによるユーザの削除

Mutationを利用したユーザの削除を行います。MutationタイプのdeleteUserを追加します。


type Mutation {
  createUser(name: String!, email: String!): User
  updateUser(id: Int!, name: String!): User
  deleteUser(id: Int!): User
}

リゾルバでdeleteUserの関数を追加します。


Mutation: {
  createUser: (_, args) => {
    return prisma.user.create({
      data: {
        name: args.name,
        email: args.email,
      },
    });
  },
  updateUser: (_, args) => {
    return prisma.user.update({
      where: {
        id: args.id,
      },
      data: {
        name: 'John Doe',
      },
    });
  },
  deleteUser: (_, args) => {
    return prisma.user.delete({
      where: { id: args.id },
    });
  },
},

Apollo Studioを利用して追加したMutationのdeleteUserを実行します。引数のidには1を指定します。

Mutationによるユーザの削除
Mutationによるユーザの削除

削除できているか確認するためのusersクエリーを実行します。削除が行われているので1名分のユーザ情報が表示さされます。

クエリーによるユーザ削除後の情報取得
クエリーによるユーザ削除後の情報取得

ここまでの動作確認でMutationを利用することで作成、更新、削除が行えることができました。Queryの動作確認は完了したのでCURD(Create, Update, Read, Delete)の操作がGraphQLで行えるようになりました。

フロントエンドからの接続

これまではApollo Studioを経由してデータの取得(Query)、データの更新(Mutation)を行なっていましたがここからはフロントエンドのReactとVueからのGraphQL上のデータの操作方法について動作確認を行なっていきます。

vanilla JavaScriptの場合

GraphQLサーバからデータを取得する際に何か特別なライブラリやパッケージが必須なわけではありません。JavaScriptのfetch関数のPOSTリクエストを利用してデータの取得を行います。

任意の場所にindex.htmlファイルを作成して以下のコードを記述します。fetch関数の中にGraphQLサーバから取得するために必要となる情報を入れています。クエリーのデータを送信するためにPOSTメソッドを利用しています。headersにはContent-Typeでapplication/jsonを指定する必要があります。bodyにクエリーを記述していますが


<!DOCTYPE html>
<html lang="ja"">
  <head>
    <meta charset="UTF-8" />
    <title>fetch関数を利用したApollo Serverからのデータ取得</title>
  </head>
  <body>
    <div id="app"></div>
  </body>
  <script>
    async function fetchBooks() {
      const response = await fetch("http://localhost:4000", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          query: `query{
                      users{
                          id
                          name
                          email
                        }
                    }`,
        }),
      });
      const data = await response.json();

      document.getElementById('app').innerHTML = `<h1>名前:${data.data.users[0].name}</h1>`;

    }
    fetchBooks();
  </script>
</html>

index.htmlファイルをブラウザで開くと画面上にGraphQLサーバから取得したユーザの名前が表示されます。vanilla JavaScriptでもGraphQLサーバからデータが取得できることが確認できました。

fetch関数によるGraphQLサーバからのデータ取得
fetch関数によるGraphQLサーバからのデータ取得

Apollo Client

先ほどvanilla JavaScriptで動作確認をしたようにfetch関数を利用してもGraphQLサーバにアクセスすることは可能ですがApollo Clientを利用します。Apollo Clientを利用することでuseQueryなどの実装する際に便利なHooksを利用することできます。Hooks以外にはキャッシュ機能や状態管理機能を持っています。Apollo ClientはReact、VueだけではなくSvelteなどの他のフレームワークで利用することができるライブラリです。

ブラウザにChromeやFirefoxを利用している場合はApollo Client Devtoolsをインストールします。Apollo Clientを利用するとデベロッパーツールのコンソールには下記のメッセージが表示されるのでリンクをクリックするとインストール画面に移動します。


Download the Apollo DevTools for a better development experience: https://chrome.google.com/webstore/detail/apollo-client-developer-t/jdkknkkbebbapilgoeccciglkfbmbnfm

Apollo Client DevtoolsではQuery, Mutationの中身、キャッシュの中身を確認することができます。

Apollo Client Devtoolsの画面
Apollo Client Devtoolsの画面

Reactの場合

Reactを利用するためにプロジェクトの作成を行います。任意の名前のプロジェクトを作成してください。


 % npx create-react-app react-graphql

プロジェクト作成後は作成したフォルダに移動して開発サーバを起動します。


 % cd react-graphql
 % npm start

ライブラリのインストール

Apollo Clientのインストールを行います。


 % npm install @apollo/client graphql

インストールしたApollo Clientを利用してindex.jsファイルで動作確認を行います。ApolloClientの引数にはuriにGraphQLサーバが起動しているURLを設定します。本文書ではApollo Serverが起動しているURLを設定しています。Apollo Clientのインスタンス化ではキャッシュを指定するのでcacheには利用するInMemoryCacheインスタンスを設定しています。作成したApollo Clientのインスタンスのqueryメソッドを利用してクエリーを実行します。クエリーの内容はgqlを利用した“(バッククオート)で囲みます。


import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import { ApolloClient, InMemoryCache, gql } from '@apollo/client';

const client = new ApolloClient({
  uri: 'http://localhost:4000',
  cache: new InMemoryCache(),
});

client
  .query({
    query: gql`
      query GetUsers {
        users {
          id
          name
          email
        }
      }
    `,
  })
  .then((result) => console.log(result));

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById('root')
);

設定後でデベロッパーツールのコンソールにGraphQLから取得したユーザデータ表示されます。

GraphQLサーバから取得したデータ
GraphQLサーバから取得したデータ

上記のデータをvanilla JavaScriptのfetch関数で取得した時のデータと比較してみます。データ以外にloadingとnetworkStatusという情報が追加されていることが確認できます。これだけを見てもfetch関数とは異なる追加機能があることがわかります。

fetch関数を利用してGraphQLサーバから取得したデータ
fetch関数を利用してGraphQLサーバから取得したデータ

Apollo Clientを利用することでGraphQLからデータを取得することができました。Reactアプリケーション全体でApollo Clientを利用できるようにApolloProviderを利用します。ApolloProviderで<App />をラップしpropsでclientを渡します。


import ReactDOM from 'react-dom';
import App from './App';
import { ApolloClient, InMemoryCache, ApolloProvider } from '@apollo/client';

const client = new ApolloClient({
  uri: 'http://localhost:4000',
  cache: new InMemoryCache(),
});

ReactDOM.render(
  <ApolloProvider client={client}>
    <App />
  </ApolloProvider>,
  document.getElementById('root')
);

ApolloProviderを設定後は他のコンポーネントでもApollo Clientを利用できるようになったのでApp.jsファイルを使って動作確認を行なっていきます。App.jsは以下のように記述します。


function App() {
  return (
    <div style={{ margin: '3em' }}>
      <h1>GraphQL</h1>
    </div>
  );
}

export default App;

useQuery Hookの利用

ここからはGraphQLにクエリーを送信する際にuseQuery Hookを利用します。useQueryの引数には実行するクエリーを指定するためGET_USERSという名前のクエリーを設定します。クエリーの名前は慣例ですべて大文字で設定しています。

useQuery Hookは自動で実行され、実行が完了するとApollo Clientからオブジェクトが戻されます。戻されるオブジェクトの中には関数なども含まれていますが、ここではその中からdata, loading, errorを利用しています。


import { gql, useQuery } from '@apollo/client';
const GET_USERS = gql`
  query GetUsers {
    users {
      id
      name
      email
    }
  }
`;

function App() {
  const { data, loading, error } = useQuery(GET_USERS);

  if (loading) return '<p>ローディング中です</p>';
  if (error) return '<p>エラーが発生しています。<p>';

  return (
    <div style={{ margin: '3em' }}>
      <h1>GraphQL</h1>
      {data.users.map((user) => (
        <div key={user.id}>Name: {user.name}</div>
      ))}
    </div>
  );
}

useQuery Hookを利用することでGraphQLサーバからデータを取得してブラウザ上に表示することができました。

useQueyの実行でGraphQLサーバから取得したデータを表示
useQueyの実行でGraphQLサーバから取得したデータを表示

Vueの場合

Vueを利用するためにVueのプロジェクトを作成します。Vue CLIを利用することもできますがViteを利用してプロジェクトの作成を行います。ViteではVueだけではなくReactもプロジェクトを作成することができます。


% npm init vite@latest vue-graphql -- --template vue

プロジェクト作成後は作成したフォルダに移動してnpm installコマンドでJacaScriptライブラリのインストールを行い開発サーバを起動します。


 % cd vue-graphql
 % npm install
 % npm run dev

Vueのバージョンは3でComposition APIを利用します。

ライブラリのインストール

Apollo Clientのインストールを行います。VueでApollo Clientを利用してクエリーを実行するためには3つのライブラリをインストールする必要があります。


 % npm install graphql graphql-tag @apollo/client

graphql-tagからgql関数をimportします。

インストールしたApollo Clientを利用してmain.jsファイルで動作確認を行います。ApolloClientの引数にはlinkとcacheを設定します。linkの値はcreateHttpLink関数にGraphQLサーバが起動しているuriを指定して作成します。本文書ではApollo Serverが起動しているURLを設定しています。Apollo Clientのインスタンス化ではキャッシュを指定するのでcacheには利用するInMemoryCacheインスタンスを設定しています。作成したApollo Clientのインスタンスのqueryメソッドを利用してクエリーを実行します。クエリーの内容はgqlを利用した“(バッククオート)で囲みます。


import { createApp } from 'vue';
import App from './App.vue';
import {
  ApolloClient,
  createHttpLink,
  InMemoryCache,
} from '@apollo/client/core';
import gql from 'graphql-tag';

const httpLink = createHttpLink({
  uri: 'http://localhost:4000/',
});

const cache = new InMemoryCache();

const apolloClient = new ApolloClient({
  link: httpLink,
  cache,
});

apolloClient
  .query({
    query: gql`
      query GetUsers {
        users {
          id
          name
          email
        }
      }
    `,
  })
  .then((result) => console.log(result));

createApp(App).mount('#app');

設定後でデベロッパーツールのコンソールにGraphQLから取得したユーザデータ表示されます。

GraphQLサーバから取得したデータ
GraphQLサーバから取得したデータ

上記のデータをvanilla JavaScriptのfetch関数で取得した時のデータと比較してみます。データ以外にloadingとnetworkStatusという情報が追加されていることが確認できます。これだけを見てもfetch関数とは異なる追加機能があることがわかります。

fetch関数を利用してGraphQLサーバから取得したデータ
fetch関数を利用してGraphQLサーバから取得したデータ

Apollo Clientを利用することでGraphQLからデータを取得することができました。Vueアプリケーション全体でApollo Clientを利用できるようにmain.jsで設定を行います。

コンポーネント内でuseQueryなどを利用することができるように@vue/apollo-composableをインストールします。


 % npm install --save @vue/apollo-composable

@vue/apollo-composableのインストール後にmain.jsファイルを以下のように更新します。


import { createApp, provide, h } from 'vue';
import { DefaultApolloClient } from '@vue/apollo-composable';
import App from './App.vue';
import {
  ApolloClient,
  createHttpLink,
  InMemoryCache,
} from '@apollo/client/core';

const httpLink = createHttpLink({
  uri: 'http://localhost:4000/',
});

const cache = new InMemoryCache();

const apolloClient = new ApolloClient({
  link: httpLink,
  cache,
});

const app = createApp({
  setup() {
    provide(DefaultApolloClient, apolloClient);
  },
  render: () => h(App),
});

app.mount('#app');

main.jsを設定後は他のコンポーネントでもApollo Clientを利用できるようになったのでApp.vueファイルを使って動作確認を行なっていきます。App.vueは以下のように記述します。


<script setup></script>

<template>
  <h1>GraphQL</h1>
</template>

useQueryの利用

ここからはGraphQLにクエリーを送信する際にuseQueryを利用します。useQueryの引数には実行するクエリーを指定するためGET_USERSという名前のクエリーを設定します。クエリーの名前は慣例ですべて大文字で設定しています。

useQuery Hookは自動で実行され、実行が完了するとApollo Clientからオブジェクトが戻されます。戻されるオブジェクトの中には関数なども含まれていますが、ここではその中からdata, loading, errorを利用しています。


<script setup>
import { useQuery } from '@vue/apollo-composable';
import gql from 'graphql-tag';
const GET_USERS = gql`
  query GetUsers {
    users {
      id
      name
      email
    }
  }
`;

const { result, loading, error } = useQuery(GET_USERS);
</script>

<template>
  <h1>GraphQL</h1>
  <div v-if="loading"><p>ローディング中です</p></div>
  <div v-else-if="error">
    <p>エラーが発生しています。</p>
    <p></p>
  </div>
  <div v-else>
    <div v-for="user in result.users" :key="user.id">Name: {{ user.name }}</div>
  </div>
</template>

useQuery Composableを利用することでGraphQLサーバからデータを取得してブラウザ上に表示することができました。

useQueyの実行でGraphQLサーバから取得したデータを表示
useQueyの実行でGraphQLサーバから取得したデータを表示

今後も更新していきます。