Node.jsとExpressを使用した実用的なGraphQLスタートガイド
序章
GraphQL は、データ要件と相互作用を記述するための直感的で柔軟な構文に基づいてクライアントアプリケーションを構築することを目的として、Facebookによって作成されたクエリ言語です。 GraphQLサービスは、タイプとそれらのタイプのフィールドを定義し、各タイプの各フィールドに関数を提供することによって作成されます。
GraphQLサービスが実行されると(通常はWebサービスのURLで)、GraphQLクエリを受信して検証および実行できます。 受信したクエリは、最初にチェックされ、定義されたタイプとフィールドのみを参照していることを確認してから、提供された関数を実行して結果を生成します。
このチュートリアルでは、 Express を使用してGraphQLサーバーを実装し、それを使用して重要なGraphQL機能を学習します。
GraphQLの機能には次のものがあります。
- 階層-クエリは、返すデータとまったく同じように見えます。
- クライアント指定のクエリ-クライアントには、サーバーから何をフェッチするかを指示する自由があります。
- 強い型-実行前に構文的におよびGraphQL型システム内でクエリを検証できます。 これは、GraphiQLなどの開発エクスペリエンスを向上させる強力なツールを活用するのにも役立ちます。
- イントロスペクティブ-GraphQL構文自体を使用して型システムにクエリを実行できます。 これは、受信データを厳密に型指定されたインターフェースに解析するのに最適であり、JSONを解析して手動でオブジェクトに変換する必要はありません。
目標
従来のREST呼び出しの主な課題の1つは、クライアントがカスタマイズされた(制限または拡張された)データのセットを要求できないことです。 ほとんどの場合、クライアントがサーバーに情報を要求すると、すべてのフィールドを取得するか、まったく取得しません。
もう1つの問題は、複数のエンドポイントの操作と保守です。 プラットフォームが成長するにつれて、その結果、その数は増加します。 したがって、クライアントは多くの場合、さまざまなエンドポイントからのデータを要求する必要があります。 GraphQL APIは、エンドポイントではなく、タイプとフィールドの観点から編成されています。 単一のエンドポイントからデータの全機能にアクセスできます。
GraphQLサーバーを構築する場合、すべてのデータのフェッチと変更に必要なURLは1つだけです。 したがって、クライアントは、必要なものを記述したクエリ文字列をサーバーに送信することで、一連のデータを要求できます。
前提条件
- Node.jsはローカルにインストールされます。これは、Node.jsのインストール方法とローカル開発環境の作成に従って実行できます。
ステップ1—ノードを使用したGraphQLのセットアップ
まず、基本的なファイル構造とサンプルコードスニペットを作成します。
まず、GraphQL
ディレクトリを作成します。
mkdir GraphQL
新しいディレクトリに移動します。
cd GraphQL
npm
プロジェクトを初期化します。
npm init -y
次に、メインファイルとなるserver.js
ファイルを作成します。
touch server.js
プロジェクトは次のようになります。
必要なパッケージは、実装時にこのチュートリアルで説明します。 次に、HTTPサーバーミドルウェアであるExpressとexpress-graphqlを使用してサーバーをセットアップします。
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ブラウザーでアクセスできるようにします。
これらの変更を行った後、ファイルを保存して閉じます。
実行されていない場合は、ノードサーバーを起動します。
node server.js
注:このチュートリアル全体を通して、server.js
を更新します。これには、最新の変更を反映するためにノードサーバーを再起動する必要があります。
Webブラウザでlocalhost:4000/graphql
にアクセスします。 Welcome to GraphiQLWebインターフェースが表示されます。
左側にクエリを入力するペインがあります。 表示するためにドラッグしてサイズ変更する必要があるクエリ変数を入力するための追加のペインがあります。 右側のペインには、クエリの実行結果が表示されます。 さらに、クエリの実行は、再生アイコンの付いたボタンを押すことで実行できます。
これまで、GraphQLのいくつかの機能と利点について説明してきました。 この次のセクションでは、GraphQLのいくつかの技術的機能のさまざまな用語と実装について詳しく説明します。 これらの機能を練習するには、Expressサーバーを使用します。
ステップ2—スキーマを定義する
GraphQLでは、スキーマがクエリとミューテーションを管理し、GraphQLサーバーで実行できるものを定義します。 スキーマは、GraphQLAPIの型システムを定義します。 クライアントがアクセスできる可能性のあるデータ(オブジェクト、フィールド、関係など)の完全なセットについて説明します。 クライアントからの呼び出しは、スキーマに対して検証および実行されます。 クライアントは、イントロスペクションを介してスキーマに関する情報を見つけることができます。 スキーマはGraphQLAPIサーバーに存在します。
GraphQLインターフェース定義言語(IDL)またはスキーマ定義言語(SDL)は、GraphQLスキーマを指定するための最も簡潔な方法です。 GraphQLスキーマの最も基本的なコンポーネントは、オブジェクトタイプです。これは、サービスからフェッチできるオブジェクトの種類と、サービスに含まれるフィールドを表します。
GraphQLスキーマ言語では、次の例のように、user
をid
、name
、および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
と、id
、name
、 age
、およびそれらのお気に入りのshark
プロパティ。
server.js
のschema
の既存のコード行を、次の新しいスキーマオブジェクトに置き換えます。
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.js
のroot
の既存のコード行を、次の新しいオブジェクトに置き換えます。
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
という名前の操作を使用して、name
、age
、およびお気に入りのshark
を持つ単一のユーザーを取得しています。 オプションで、age
とshark
が必要ない場合にのみ、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" } } }
これにより、1
、Brian
のid
に一致する単一のユーザーが返されます。 また、要求されたname
、age
、および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つの問題があります。 userA
とuserB
の両方で同じフィールドを繰り返しています。 クエリ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 Shark
–Faith
およびJoy
と一致する2人のユーザーが返されます。
ステップ7—ミューテーションの定義
これまで、クエリ、つまりデータを取得する操作を扱ってきました。 ミューテーションは、データの作成、削除、更新を処理するGraphQLの2番目の主要な操作です。
突然変異を実行する方法のいくつかの例に焦点を当てましょう。 たとえば、ユーザーをid == 1
で更新し、age
、name
を変更してから、新しいユーザーの詳細を返します。
スキーマを更新して、既存のコード行に加えてミューテーションタイプを含めます。
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 } `);
getUser
とretrieveUsers
の後に、ユーザーの更新を処理するための新しい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" } } }
ユーザーを更新するための変更の後、新しいユーザーの詳細を取得します。
1
のid
を持つユーザーがBrian
(age 21
)からKeavin
(age 27
)に更新されました。
結論
このガイドでは、GraphQLの基本的な概念から、かなり複雑な例までを取り上げました。 これらの例のほとんどは、RESTを操作したユーザーのGraphQLとRESTの違いを示しています。
GraphQLの詳細については、公式ドキュメントを確認してください。