序章

GraphQL は、データ要件と相互作用を記述するための直感的で柔軟な構文に基づいてクライアントアプリケーションを構築することを目的として、Facebookによって作成されたクエリ言語です。 GraphQLサービスは、タイプとそれらのタイプのフィールドを定義し、各タイプの各フィールドに関数を提供することによって作成されます。

GraphQLサービスが実行されると(通常はWebサービスのURLで)、GraphQLクエリを受信して検証および実行できます。 受信したクエリは、最初にチェックされ、定義されたタイプとフィールドのみを参照していることを確認してから、提供された関数を実行して結果を生成します。

このチュートリアルでは、 Express を使用してGraphQLサーバーを実装し、それを使用して重要なGraphQL機能を学習します。

GraphQL

GraphQLの機能には次のものがあります。

  • 階層-クエリは、返すデータとまったく同じように見えます。

  • クライアント指定のクエリ-クライアントには、サーバーから何をフェッチするかを指示する自由があります。

  • 強い型-実行前に構文的におよびGraphQL型システム内でクエリを検証できます。 これは、GraphiQLなどの開発エクスペリエンスを向上させる強力なツールを活用するのにも役立ちます。

  • イントロスペクティブ-GraphQL構文自体を使用して型システムにクエリを実行できます。 これは、受信データを厳密に型指定されたインターフェースに解析するのに最適であり、JSONを解析して手動でオブジェクトに変換する必要はありません。

目標

従来のREST呼び出しの主な課題の1つは、クライアントがカスタマイズされた(制限または拡張された)データセットを要求できないことです。 ほとんどの場合、クライアントがサーバーに情報を要求すると、すべてのフィールドを取得するか、まったく取得しません。

もう1つの問題は、複数のエンドポイントの操作と保守です。 プラットフォームが成長するにつれて、その結果、その数は増加します。 したがって、クライアントは多くの場合、さまざまなエンドポイントからのデータを要求する必要があります。 GraphQL APIは、エンドポイントではなく、タイプとフィールドの観点から編成されています。 単一のエンドポイントからデータの全機能にアクセスできます。

GraphQLサーバーを構築する場合、すべてのデータのフェッチと変更に必要なURLは1つだけです。 したがって、クライアントは、必要なものを記述したクエリ文字列をサーバーに送信することで、一連のデータを要求できます。

前提条件

ステップ1—ノードを使用したGraphQLのセットアップ

まず、基本的なファイル構造とサンプルコードスニペットを作成します。

まず、GraphQLディレクトリを作成します。

  1. mkdir GraphQL

新しいディレクトリに移動します。

  1. cd GraphQL

npmプロジェクトを初期化します。

  1. npm init -y

次に、メインファイルとなるserver.jsファイルを作成します。

  1. touch server.js

プロジェクトは次のようになります。

Listing of directory contents.

必要なパッケージは、実装時にこのチュートリアルで説明します。 次に、HTTPサーバーミドルウェアであるExpressexpress-graphqlを使用してサーバーをセットアップします。

  1. npm install graphql express express-graphql

テキストエディタでserver.jsを開き、次のコード行を追加します。

server.js
var express = require('express');
var graphqlHTTP = require('express-graphql');
var { buildSchema } = require('graphql');

// Initialize a GraphQL schema
var schema = buildSchema(`
  type Query {
    hello: String
  }
`);

// Root resolver
var root = { 
  hello: () => 'Hello world!'
};

// Create an express server and a GraphQL endpoint
var app = express();
app.use('/graphql', graphqlHTTP({
  schema: schema,  // Must be provided
  rootValue: root,
  graphiql: true,  // Enable GraphiQL when server endpoint is accessed in browser
}));
app.listen(4000, () => console.log('Now browse to localhost:4000/graphql'));

注:このコードは、以前のバージョンのexpress-graphqlで記述されています。 v0.10.0より前では、var graphqlHTTP = require('express-graphql');を使用できました。 v0.10.0以降は、var { graphqlHTTP } = require('express-graphql');を使用する必要があります。

このスニペットはいくつかのことを実行します。 requireを使用して、インストールされたパッケージを含めます。 また、一般的なschemaおよびroot値を初期化します。 さらに、/graphqlにエンドポイントを作成し、Webブラウザーでアクセスできるようにします。

これらの変更を行った後、ファイルを保存して閉じます。

実行されていない場合は、ノードサーバーを起動します。

  1. node server.js

注:このチュートリアル全体を通して、server.jsを更新します。これには、最新の変更を反映するためにノードサーバーを再起動する必要があります。

Webブラウザでlocalhost:4000/graphqlにアクセスします。 Welcome to GraphiQLWebインターフェースが表示されます。

左側にクエリを入力するペインがあります。 表示するためにドラッグしてサイズ変更する必要があるクエリ変数を入力するための追加のペインがあります。 右側のペインには、クエリの実行結果が表示されます。 さらに、クエリの実行は、再生アイコンの付いたボタンを押すことで実行できます。

Screenshot of GraphiQL web interface

これまで、GraphQLのいくつかの機能と利点について説明してきました。 この次のセクションでは、GraphQLのいくつかの技術的機能のさまざまな用語と実装について詳しく説明します。 これらの機能を練習するには、Expressサーバーを使用します。

ステップ2—スキーマを定義する

GraphQLでは、スキーマがクエリとミューテーションを管理し、GraphQLサーバーで実行できるものを定義します。 スキーマは、GraphQLAPIの型システムを定義します。 クライアントがアクセスできる可能性のあるデータ(オブジェクト、フィールド、関係など)の完全なセットについて説明します。 クライアントからの呼び出しは、スキーマに対して検証および実行されます。 クライアントは、イントロスペクションを介してスキーマに関する情報を見つけることができます。 スキーマはGraphQLAPIサーバーに存在します。

GraphQLインターフェース定義言語(IDL)またはスキーマ定義言語(SDL)は、GraphQLスキーマを指定するための最も簡潔な方法です。 GraphQLスキーマの最も基本的なコンポーネントは、オブジェクトタイプです。これは、サービスからフェッチできるオブジェクトの種類と、サービスに含まれるフィールドを表します。

GraphQLスキーマ言語では、次の例のように、useridname、およびageで表すことができます。

type User {
  id: ID!
  name: String!
  age: Int
}

JavaScriptでは、GraphQLスキーマ言語からSchemaオブジェクトを構築するbuildSchema関数を使用します。 上記と同じuserを表すとすると、次の例のようになります。

var schema = buildSchema(`
  type User {
    id: Int
    name: String!
    age: Int
  }
`);

タイプの構築

buildSchema内でさまざまなタイプを定義できます。ほとんどの場合、type Query {...}type Mutation {...}です。 type Query {...}は、GraphQLクエリにマップされる関数を保持するオブジェクトであり、データのフェッチに使用されます(RESTのGETと同等)。 type Mutation {...}は、ミューテーションにマップされ、データの作成、更新、または削除に使用される関数を保持します(RESTのPOST、UPDATE、およびDELETEと同等)。

いくつかの妥当な型を追加することにより、スキーマを少し複雑にします。 たとえば、userと、idnameage、およびそれらのお気に入りのsharkプロパティ。

server.jsschemaの既存のコード行を、次の新しいスキーマオブジェクトに置き換えます。

server.js
// Initialize a GraphQL schema
var schema = buildSchema(`
  type Query {
    user(id: Int!): Person
    users(shark: String): [Person]
  },
  type Person {
    id: Int
    name: String
    age: Int
    shark: String
  }
`);

上記の興味深い構文に気付くかもしれません。[Person]Person型の配列を返すことを意味し、user(id: Int!)の感嘆符はidを指定する必要があることを意味します。 usersクエリは、オプションのshark変数を取ります。

ステップ3—リゾルバーの定義

リゾルバーは、操作を実際の関数にマッピングする役割を果たします。 type Query内には、usersという操作があります。 この操作をroot内の同じ名前の関数にマップします。

また、この機能のサンプルユーザーをいくつか作成します。

次の新しいコード行をbuildSchemaコード行の直後、rootコード行の前のserver.jsに追加します。

server.js
...
// Sample users
var users = [
  {
    id: 1,
    name: 'Brian',
    age: '21',
    shark: 'Great White Shark'
  },
  {
    id: 2,
    name: 'Kim',
    age: '22',
    shark: 'Whale Shark'
  },
  {
    id: 3,
    name: 'Faith',
    age: '23',
    shark: 'Hammerhead Shark'
  },
  {
    id: 4,
    name: 'Joseph',
    age: '23',
    shark: 'Tiger Shark'
  },
  {
    id: 5,
    name: 'Joy',
    age: '25',
    shark: 'Hammerhead Shark'
  }
];

// Return a single user
var getUser = function(args) {
  // ...
}

// Return a list of users
var retrieveUsers = function(args) { 
  // ...
}
...

server.jsrootの既存のコード行を、次の新しいオブジェクトに置き換えます。

server.js
// Root resolver
var root = { 
  user: getUser,  // Resolver function to return user with specific id
  users: retrieveUsers
};

コードを読みやすくするには、ルートリゾルバーにすべてを積み上げるのではなく、個別の関数を作成します。 どちらの関数も、クライアントクエリから変数を運ぶオプションのargsパラメータを取ります。 リゾルバーの実装を提供し、それらの機能をテストしてみましょう。

以前にserver.jsに追加したgetUserおよびretrieveUsersのコード行を次のように置き換えます。

server.js
// Return a single user (based on id)
var getUser = function(args) {
  var userID = args.id;
  return users.filter(user => user.id == userID)[0];
}

// Return a list of users (takes an optional shark parameter)
var retrieveUsers = function(args) {
  if (args.shark) {
    var shark = args.shark;
    return users.filter(user => user.shark === shark);
  } else {
    return users;
  }
}

Webインターフェイスで、入力ペインに次のクエリを入力します。

query getSingleUser {
  user {
    name
    age
    shark
  }
}

次の出力が表示されます。

Output
{ "errors": [ { "message": "Cannot query field \"user\" on type \"Query\".", "locations": [ { "line": 2, "column": 3 } ] } ] }

上記の例では、getSingleUserという名前の操作を使用して、nameage、およびお気に入りのsharkを持つ単一のユーザーを取得しています。 オプションで、agesharkが必要ない場合にのみ、nameが必要であることを指定できます。

公式ドキュメントによると、内容を解読するのではなく、名前でコードベース内のクエリを識別するのが最も簡単です。

このクエリは必要なidを提供せず、GraphQLは説明的なエラーメッセージを表示します。 ここで、正しいクエリを作成します。 変数と引数の使用に注意してください。

Webインターフェイスで、入力ペインのコンテンツを次の修正されたクエリに置き換えます。

query getSingleUser($userID: Int!) {
  user(id: $userID) {
    name
    age
    shark
  }
}

Webインターフェースを使用したまま、変数ペインのコンテンツを次のように置き換えます。

Query Variables
{ "userID": 1 }

次の出力が表示されます。

Output
{ "data": { "user": { "name": "Brian", "age": 21, "shark": "Great White Shark" } } }

これにより、1Brianidに一致する単一のユーザーが返されます。 また、要求されたnameage、およびsharkフィールドを返します。

ステップ4—エイリアスの定義

2人の異なるユーザーを取得する必要がある状況では、各ユーザーをどのように識別するのか疑問に思われるかもしれません。 GraphQLでは、異なる引数を使用して同じフィールドを直接クエリすることはできません。 これをデモンストレーションしましょう。

Webインターフェイスで、入力ペインのコンテンツを次のように置き換えます。

query getUsersWithAliasesError($userAID: Int!, $userBID: Int!) {
  user(id: $userAID) {
    name
    age
    shark
  },
  user(id: $userBID) {
    name
    age
    shark
  }
}

Webインターフェースを使用したまま、変数ペインのコンテンツを次のように置き換えます。

Query Variables
{ "userAID": 1, "userBID": 2 }

次の出力が表示されます。

Output
{ "errors": [ { "message": "Fields \"user\" conflict because they have differing arguments. Use different aliases on the fields to fetch both if this was intentional.", "locations": [ { "line": 2, "column": 3 }, { "line": 7, "column": 3 } ] } ] }

エラーは説明的であり、エイリアスの使用を示唆しています。 実装を修正しましょう。

Webインターフェイスで、入力ペインのコンテンツを次の修正されたクエリに置き換えます。

query getUsersWithAliases($userAID: Int!, $userBID: Int!) {
  userA: user(id: $userAID) {
    name
    age
    shark
  },
  userB: user(id: $userBID) {
    name
    age
    shark
  }
}

Webインターフェースを使用している間に、変数ペインに次のものが含まれていることを確認します。

Query Variables
{ "userAID": 1, "userBID": 2 }

次の出力が表示されます。

Output
{ "data": { "userA": { "name": "Brian", "age": 21, "shark": "Great White Shark" }, "userB": { "name": "Kim", "age": 22, "shark": "Whale Shark" } } }

これで、各ユーザーをフィールドで正しく識別できます。

ステップ5—フラグメントの作成

上記のクエリはそれほど悪くはありませんが、1つの問題があります。 userAuserBの両方で同じフィールドを繰り返しています。 クエリDRYを作成するものを見つけることができました。 GraphQLには、 Fragments と呼ばれる再利用可能なユニットが含まれています。これにより、フィールドのセットを作成し、必要に応じてクエリに含めることができます。

Webインターフェイスで、変数ペインのコンテンツを次のように置き換えます。

query getUsersWithFragments($userAID: Int!, $userBID: Int!) {
  userA: user(id: $userAID) {
    ...userFields
  },
  userB: user(id: $userBID) {
    ...userFields
  }
}

fragment userFields on Person {
  name
  age
  shark
}

Webインターフェースを使用している間に、変数ペインに次のものが含まれていることを確認します。

Query Variables
{ "userAID": 1, "userBID": 2 }

次の出力が表示されます。

Output
{ "data": { "userA": { "name": "Brian", "age": 21, "shark": "Great White Shark" }, "userB": { "name": "Kim", "age": 22, "shark": "Whale Shark" } } }

userFieldsというフラグメントを作成しました。このフラグメントは、type Personにのみ適用でき、それを使用してユーザーを取得します。

ステップ6—ディレクティブの定義

ディレクティブを使用すると、変数を使用してクエリの構造と形状を動的に変更できます。 ある時点で、スキーマを変更せずに一部のフィールドをスキップまたは含めることができます。 使用可能な2つのディレクティブは次のとおりです。

  • @include(if: Boolean)-引数がtrueの場合にのみ、このフィールドを結果に含めます。
  • @skip(if: Boolean)-引数がtrueの場合、このフィールドをスキップします。

Hammerhead Sharkのファンであるが、idを含め、ageフィールドをスキップするユーザーを取得するとします。 変数を使用してsharkを渡し、包含およびスキップ機能のディレクティブを使用できます。

Webインターフェイスで、入力ペインをクリアし、以下を追加します。

query getUsers($shark: String, $age: Boolean!, $id: Boolean!) {
  users(shark: $shark){
    ...userFields
  }
}

fragment userFields on Person {
  name
  age @skip(if: $age)
  id @include(if: $id)
}

Webインターフェースを使用したまま、変数ペインをクリアして、以下を追加します。

Query Variables
{ "shark": "Hammerhead Shark", "age": true, "id": true }

次の出力が表示されます。

Output
{ "data": { "users": [ { "name": "Faith", "id": 3 }, { "name": "Joy", "id": 5 } ] } }

これにより、sharkの値がHammerhead SharkFaithおよびJoyと一致する2人のユーザーが返されます。

ステップ7—ミューテーションの定義

これまで、クエリ、つまりデータを取得する操作を扱ってきました。 ミューテーションは、データの作成、削除、更新を処理するGraphQLの2番目の主要な操作です。

突然変異を実行する方法のいくつかの例に焦点を当てましょう。 たとえば、id == 1でユーザーを更新し、agenameを変更してから、新しいユーザーの詳細を返します。

スキーマを更新して、既存のコード行に加えてミューテーションタイプを含めます。

server.js
// Initialize a GraphQL schema
var schema = buildSchema(`
  type Query {
    user(id: Int!): Person
    users(shark: String): [Person]
  },
  type Person {
    id: Int
    name: String
    age: Int
    shark: String
  }
  # newly added code
  type Mutation {
    updateUser(id: Int!, name: String!, age: String): Person
  }
`);

getUserretrieveUsersの後に、ユーザーの更新を処理する新しいupdateUser関数を追加します。

server.js
// Update a user and return new user details
var updateUser = function({id, name, age}) {
  users.map(user => {
    if (user.id === id) {
      user.name = name;
      user.age = age;
      return user;
    }
  });
  return users.filter(user => user.id === id)[0];
}

また、関連するリゾルバー関数でルートリゾルバーを更新します。

server.js
// Root resolver
var root = { 
  user: getUser,
  users: retrieveUsers,
  updateUser: updateUser  // Include mutation function in root resolver
};

これらが最初のユーザーの詳細であると仮定します。

Output
{ "data": { "user": { "name": "Brian", "age": 21, "shark": "Great White Shark" } } }

Webインターフェイスで、次のクエリを入力ペインに追加します。

mutation updateUser($id: Int!, $name: String!, $age: String) {
  updateUser(id: $id, name:$name, age: $age){
    ...userFields
  }
}

fragment userFields on Person {
  name
  age
  shark
}

Webインターフェースを使用したまま、変数ペインをクリアして、以下を追加します。

Query Variables
{ "id": 1, "name": "Keavin", "age": "27" }

次の出力が表示されます。

Output
{ "data": { "updateUser": { "name": "Keavin", "age": 27, "shark": "Great White Shark" } } }

ユーザーを更新するための変更の後、新しいユーザーの詳細を取得します。

1idを持つユーザーがBrianage 21)からKeavinage 27)に更新されました。

結論

このガイドでは、GraphQLの基本的な概念から、かなり複雑な例までを取り上げました。 これらの例のほとんどは、RESTを操作したユーザーのGraphQLとRESTの違いを示しています。

GraphQLの詳細については、公式ドキュメントを確認してください。