AppSyncを理解するために、GraphQLについてまとめてみた
おはようございます。
ベンジャミン木村です!今回はAppSyncを理解するために、まずはGraphQLについて調べたので、そのまとめを記載させていただきます!
手を動かす部分はChatGPTに壁打ちしながら、理解していきました。その手順も記載しておりますので、ぜひ、私と同じ初心者の方はご覧いただけますと幸いです!
目次
- GraphQLとは?
- スキーマ、リゾルバとは?
- 1. 事前準備
- 2. まずはHello, GraphQL!を出力しよう!
- 3. データストアからデータを出力しよう
- 4. Mutationを理解しよう
- まとめ
GraphQLとは?
GraphQLとは、APIのクエリ言語およびランタイムで、必要なデータを柔軟にリクエストし、取得できる仕組みです。
通常APIとしてよく使われるREST APIは、エンドポイントごとに決まったデータがまとまって送られてきますが、GraphQLでは一つのエンドポイントに対して、クライアントが「この部分のデータだけください」と細かく指定できます。
より具体的な説明を下記にて記載します。
REST API
- 必要なデータに応じて、複数のエンドポイントを呼び出す必要がある。
- 例)
/users
→ ユーザー情報を取得/posts
→ 投稿情報を取得/comments
→ コメント情報を取得
- 例)
- データ取得時に不要なデータも含まれるため、レスポンスのデータ量が増える。
- 例) ユーザーID 1のデータを取得する(
/users?id=1
)ときのレスポンス{ id: 1 title: 今日の投稿 post: 初めましてベンジャミンの木村です… }
- 例) ユーザーID 1のデータを取得する(
GraphQL
- すべてのリクエストを1つのエンドポイント(
/graphql
)で処理する。 - 必要なデータだけを取得できる。
- 例)ユーザーID 1の投稿タイトルだけを下記クエリで取得可能です。
query {
user(id: 1) {
posts {
title
}
}
}
- 例)ユーザーID 1の投稿タイトルだけを下記クエリで取得可能です。
まとめると、GraphQLは1つのエンドポイントで必要なデータを自由にリクエストできるのが特徴です。
スキーマ、リゾルバとは?
1. スキーマとは?:
「GraphQLスキーマは、APIのエントリーポイントを定義し、どのデータが取得可能でどのように構造化されるかを記述します。」
2. リゾルバとは?:
「リゾルバは、スキーマで定義されたフィールドごとにデータ取得ロジックを実装する役割を持っています。」
GraphQLにおけるスキーマとリゾルバの役割をわかりやすく説明するため、レストランを例に挙げます。
- レストランのメニューには「料理名」と「値段」が書かれています。
- お客さんは「ハンバーガーを注文したい!」とリクエストを出します。
- レストランは注文に応じて、料理を提供します。
この時、GraphQLのスキーマは、この「メニュー表」にあたります。
- お客さん(クライアント)は、「どんなデータが欲しいか」をスキーマを見てリクエストします。
- スキーマは、「このリクエストにはこういうデータを返す」と決めておくものになります。
また、リゾルバは「注文を受けて料理を用意するシェフ」のようなものです。
- クライアント(お客さん)がスキーマ(メニュー)に沿って「ハンバーガーください」とリクエストを送ります。
- リゾルバ(シェフ)が「そのリクエストに応じたデータ(食事)」を提供します。
リクエスト → リゾルバ → データの流れを作る、という仕組みです。
これを実際のコードに書き換えると下記になります。
「コードにするとよくわからない…」と思ったと思います。今はなんとなくの理解で大丈夫ですので、この後、実際に手を動かしながら、理解していきましょう!
1. 事前準備
※今回作業は全てローカル環境で実施してください。AWSコンソールは開かないので、よろしくお願いいたします。
1-1. Node.jsのインストール
Node.jsがインストールされていない場合、Node.js公式サイトからインストールしてください。
1-2. プロジェクトの作成
下記コマンド実行すると、graphql-hands-onディレクトリにpackage.json、node_modulesを出力されます。
mkdir graphql-hands-on
cd graphql-hands-on
npm init -y
1-3. 必要なパッケージのインストール
下記コマンドを実行し、GraphQLに必要なパッケージをインストールしてください。
npm install @apollo/server graphql express cors
2. まずはHello, GraphQL!を出力しよう!
まずは、下記図のようにクライアントがhelloのクエリを送ったら、GraphQLより「Hello, GraphQL!」というレスポンスが返ってくるように、記述しましょう!
2-1. 下記コマンドを実行し、index.jsファイルを作成する
touch index.js
2-2. 作成したindex.jsに下記コードを記述する
const { ApolloServer } = require('@apollo/server');
const { startStandaloneServer } = require('@apollo/server/standalone');
// GraphQLスキーマ
const typeDefs = `#graphql
type Query {
hello: String
}
`
// リゾルバ
const resolvers = {
Query: {
hello: () => "Hello, GraphQL!"
}
}
// サーバーの起動
async function startServer() {
const server = new ApolloServer({ typeDefs, resolvers });
const { url } = await startStandaloneServer(server, {
listen: { port: 4000 }
});
console.log(`Server ready at ${url}`);
}
startServer().catch(err => {
console.error('Error starting server:', err);
});
コードの解説は下記にてさせていただきます。
1. 必要なモジュールのインポート
const { ApolloServer } = require('@apollo/server'); const { startStandaloneServer } = require('@apollo/server/standalone');
ApolloServer
- GraphQL サーバーの主要なクラスで、スキーマ(
typeDefs
)とリゾルバ(resolvers
)を指定してサーバーを構築します。
- GraphQL サーバーの主要なクラスで、スキーマ(
startStandaloneServer
- 単体で動作する Apollo Server を起動するための関数で、ポート番号などを指定できます。
2. GraphQL スキーマの定義
const typeDefs = `#graphql type Query { hello: String } `
- スキーマ (
typeDefs
):type Query
:- GraphQL のエントリーポイントで、クライアントがリクエストできるクエリを定義します。今回はhello(String 型のフィールド)で、リクエストすると文字列を返します用にスキーマを定義しています。
3. リゾルバの定義
const resolvers = { Query: { hello: () => "Hello, GraphQL!" } }
- リゾルバ (
resolvers
):- クライアントから送られたクエリを処理し、データを返す関数を定義します。
Query.hello
:hello
フィールドにクエリを実行すると、常に"Hello, GraphQL!"
を返す関数です。
4. サーバーの起動
async function startServer() { const server = new ApolloServer({ typeDefs, resolvers }); const { url } = await startStandaloneServer(server, { listen: { port: 4000 } }); console.log(`Server ready at ${url}`); }
ApolloServer
のインスタンス作成- スキーマとリゾルバを設定してサーバーを作成します。
startStandaloneServer
を使ったサーバーの起動- サーバーをポート
4000
番でリッスンし、URL を返します。 - サーバーが正常に起動すると、コンソールにアクセス URL が表示されます(例:
http://localhost:4000
)。
- サーバーをポート
5. エラー処理
startServer().catch(err => { console.error('Error starting server:', err); });
- サーバーの起動中にエラーが発生した場合、エラーメッセージをコンソールに出力します。
2-3. サーバーを起動する
下記コマンドをターミナルで実行してください。
node index.js
上記、コマンドを実行すると、GraphQL Playground(またはApollo Sandbox)と呼ばれる対話型のGraphQL IDEが立ち上がりますので、
ターミナルに表示されたhttp://localhost:4000/をブラウザで開いてください。
2-4. クエリを実行する
GraphQL Playgroundで下記クエリを実行してください。「Hello, GraphQL!」とレスポンスが返ってくることでしょう!
query {
hello
}
3. データストアからデータを出力しよう
今回はDBを用意するところまではせず、DBのデータを仮のデータストアとして用意します。
今回は、クライアントがmenuItemのnameとpriceをリクエストし、DataStoreに保存されているmenuItemのnameとpriceのデータをレスポンスするコードを書いていきます。
変更前
const { ApolloServer } = require('@apollo/server');
const { startStandaloneServer } = require('@apollo/server/standalone');
// GraphQLスキーマ
const typeDefs = `#graphql
type Query {
hello: String
}
`;
// リゾルバ
const resolvers = {
Query: {
hello: () => "hello GraphQL!"
}
};
// サーバーの起動
async function startServer() {
const server = new ApolloServer({ typeDefs, resolvers });
const { url } = await startStandaloneServer(server, {
listen: { port: 4000 }
});
console.log(`Server ready at ${url}`);
}
startServer().catch(err => {
console.error('Error starting server:', err);
});
変更後
const { ApolloServer } = require('@apollo/server');
const { startStandaloneServer } = require('@apollo/server/standalone');
// データストア(簡易的にメモリ内で管理)
const menuItems = [
{
id: "1",
name: "マルゲリータピザ",
description: "トマトソースとバジル、モッツァレラチーズのピザ",
category: "メインディッシュ",
price: 1200,
isVegetarian: true
},
{
id: "2",
name: "カルボナーラ",
description: "濃厚なクリームソースにベーコンと黒胡椒を添えたパスタ",
category: "メインディッシュ",
price: 1100,
isVegetarian: false
},
{
id: "3",
name: "シーザーサラダ",
description: "新鮮なレタスにチーズとクルトンをトッピング",
category: "サラダ",
price: 800,
isVegetarian: true
},
{
id: "4",
name: "ガトーショコラ",
description: "濃厚なチョコレートケーキ",
category: "デザート",
price: 600,
isVegetarian: true
},
{
id: "5",
name: "エスプレッソ",
description: "濃厚で香り高いイタリアンコーヒー",
category: "ドリンク",
price: 400,
isVegetarian: true
},
{
id: "6",
name: "クラシックハンバーガー",
description: "ジューシーなビーフパティと新鮮な野菜を使用した定番のハンバーガー",
category: "メインディッシュ",
price: 900,
isVegetarian: false
}
];
// GraphQLスキーマ
const typeDefs = `#graphql
type Query {
menuItems: [MenuItem!]!
hello: String
}
type MenuItem {
id: ID!
name: String!
description: String!
category: String!
price: Int!
isVegetarian: Boolean!
}
`;
// リゾルバ
const resolvers = {
Query: {
menuItems: () => menuItems,
hello: () => "hello GraphQL!"
}
};
// サーバーの起動
async function startServer() {
const server = new ApolloServer({ typeDefs, resolvers });
const { url } = await startStandaloneServer(server, {
listen: { port: 4000 }
});
console.log(`Server ready at ${url}`);
}
startServer().catch(err => {
console.error('Error starting server:', err);
});
コードの内容は以下にて解説させていただきます。
1. データストア部分
const menuItems = [
{
id: "1",
name: "マルゲリータピザ",
description: "トマトソースとバジル、モッツァレラチーズのピザ",
category: "メインディッシュ",
price: 1200,
isVegetarian: true
},
// ... 他のメニュー
];- 役割
- DBのデータを仮想データとして作成したものです。メニューのデータを格納しています。
- 構造
id
: メニュー項目の一意な識別子。name
: メニュー名(例: マルゲリータピザ)。description
: メニューの説明文。category
: メニューのカテゴリ(例: メインディッシュ、ドリンク)。price
: メニューの価格(数値形式)。isVegetarian
: ベジタリアン向けかどうか(true
またはfalse
)。
2. GraphQLスキーマ部分
- 構造
const typeDefs = `#graphql
type Query {
menuItems: [MenuItem!]!
hello: String
}
type MenuItem {
id: ID!
name: String!
description: String!
category: String!
price: Int!
isVegetarian: Boolean!
}
`;type Query
: データを取得するためのクエリ(操作)を定義menuItems
: メニュー項目のリストを返すクエリ。- 戻り値は
[MenuItem!]!
で、MenuItem
型の配列を必ず返すことを意味します。
- 戻り値は
- ※helloの部分は先ほどと同様の記述です
type MenuItem
: メニュー項目のデータ構造を定義する型- 各フィールドは
id
,name
,description
,category
,price
,isVegetarian
のように、メニュー項目のデータ型と必須性(!
)を指定しています。
- 各フィールドは
3. リゾルバ部分
- 構造
const resolvers = {
Query: {
menuItems: () => menuItems,
hello: () => "Welcome to our restaurant!"
}
};menuItems
:- クライアントが
menuItems
クエリをリクエストしたときに、menuItems
配列のデータを返す関数。 - ※helloの部分は先ほどと同様のものです
- クライアントが
※サーバーの起動部分は先ほどと同様なので説明の記載は省略いたします。
3-2. サーバーを起動する
先ほどと同じように、下記コマンドをターミナルで実行し、http://localhost:4000/をブラウザで開いてください。
node index.js
3-3. クエリを実行する
GraphQL Playgroundで下記クエリを実行してください。menuItemsのnameとpriceだけ一覧で返ってくることでしょう!
query {
menuItems {
name
price
}
}
4. Mutationを理解しよう
データの作成・更新・削除などの操作を行う、Mutationについても理解しましょう!
まずは、先ほどのコードにMutation要素を追加した下記コードを記載ください。コードは新しいデータをデータストアに追加し、それを表示する記述にしています。(便宜上、データストアのデータ量が多いので、データストアのデータはid=1のみにしています。)
変更前
const { ApolloServer } = require('@apollo/server');
const { startStandaloneServer } = require('@apollo/server/standalone');
// データストア(簡易的にメモリ内で管理)
const menuItems = [
{
id: "1",
name: "マルゲリータピザ",
description: "トマトソースとバジル、モッツァレラチーズのピザ",
category: "メインディッシュ",
price: 1200,
isVegetarian: true
}
];
// GraphQLスキーマ定義
const typeDefs = `#graphql
type Query {
menuItems: [MenuItem!]!
hello: String
}
type MenuItem {
id: ID!
name: String!
description: String!
category: String!
price: Int!
isVegetarian: Boolean!
}
`;
// リゾルバの実装
const resolvers = {
Query: {
menuItems: () => menuItems,
hello: () => "Welcome to our restaurant!"
},
};
// サーバー起動
async function startServer() {
const server = new ApolloServer({ typeDefs, resolvers });
const { url } = await startStandaloneServer(server, {
listen: { port: 4000 }
});
console.log(`Server ready at ${url}`);
}
startServer().catch(err => {
console.error('Error starting server:', err);
});
変更後
const { ApolloServer } = require('@apollo/server');
const { startStandaloneServer } = require('@apollo/server/standalone');
// データストア(簡易的にメモリ内で管理)
const menuItems = [
{
id: "1",
name: "マルゲリータピザ",
description: "トマトソースとバジル、モッツァレラチーズのピザ",
category: "メインディッシュ",
price: 1200,
isVegetarian: true
}
];
// GraphQLスキーマ定義
const typeDefs = `#graphql
type Query {
menuItems: [MenuItem!]!
hello: String
}
type Mutation {
addMenuItem(
name: String!,
description: String!,
category: String!,
price: Int!,
isVegetarian: Boolean!
): MenuItem!
}
type MenuItem {
id: ID!
name: String!
description: String!
category: String!
price: Int!
isVegetarian: Boolean!
}
`;
// リゾルバの実装
const resolvers = {
Query: {
menuItems: () => menuItems,
hello: () => "Welcome to our restaurant!"
},
Mutation: {
addMenuItem: (_, args) => {
const newMenuItem = {
id: String(menuItems.length + 1),
...args
};
menuItems.push(newMenuItem);
return newMenuItem;
}
}
};
// サーバー起動
async function startServer() {
const server = new ApolloServer({ typeDefs, resolvers });
const { url } = await startStandaloneServer(server, {
listen: { port: 4000 }
});
console.log(`Server ready at ${url}`);
}
startServer().catch(err => {
console.error('Error starting server:', err);
});
コードの内容は以下にて解説させていただきます。
1. スキーマ (type Mutation
)
type Mutation {
addMenuItem(
name: String!,
description: String!,
category: String!,
price: Int!,
isVegetarian: Boolean!
): MenuItem!type Mutation
の中にaddMenuItem
という操作を定義しています。- 目的:
- この操作を使うことで、新しいメニュー(例: ハンバーガー)をシステムに追加できるようになります。
- 構造:(引数により受け取るデータと、関数(リゾルバ)からの返り値を定義します)
- 受け取るデータ:
name
description
category
price
isVegetarian
- 戻り値:
- 新しく作成された
MenuItem
オブジェクトを返します(追加したメニューの内容)。
- 新しく作成された
- 受け取るデータ:
2. リゾルバ
Mutation の処理内容:
- 目的
addMenuItem
という関数が、受け取ったデータを使ってメニューを作成します。
- 具体的な処理の流れ
- 1. 新しいメニューの作成:
const newMenuItem = { id: String(menuItems.length + 1), ...args };
id
は現在の配列(menuItems
)の長さに 1 を足して自動生成します。- それ以外の…argsで渡される項目(
name
,description
,…など)は受け取ったデータをそのまま使用します。
- 2. データストア(配列)への保存
menuItems.push(newMenuItem);
- 作成した
newItem
をmenuItems
という配列に追加します。
- 3. 作成したメニューを返す
return newMenuItem;
- 新しく作成したメニュー(
newItem
)をクライアントに返します。
- 1. 新しいメニューの作成:
4-2. サーバーを起動する
先ほどと同じように、下記コマンドをターミナルで実行し、http://localhost:4000/をブラウザで開いてください。
node index.js
4-3. クエリを実行する
GraphQL Playgroundで下記クエリを実行してください。データを追加し、そのデータが追加された状態でレスポンスが返ってこればOK!
-- データの追加部分
mutation {
addMenuItem(
name: "ハンバーガー",
description: "ジューシーなビーフを使ったバーガー",
category: "Main",
price: 1200,
isVegetarian: false
){
id
name
description
price
isVegetarian
}
}
-- データ追加後のMenuItemsを表示
query {
menuItems {
name
price
}
}
まとめ
いかがでしたでしょうか?
今回はAppSyncを理解するために、GraphQLの基礎的な部分について説明させていただきました。
他にもGraphQLには様々な構成要素がありますので、知りたい方は下記記事が大変わかりやすかったので、ご参照ください。
https://qiita.com/h-i-ist/items/805ab88f0242740695ed
次はAppSyncについて記述したいと思います!乞うご期待!