AppSyncを理解するために、GraphQLについてまとめてみた

technologies

おはようございます。

ベンジャミン木村です!今回はAppSyncを理解するために、まずはGraphQLについて調べたので、そのまとめを記載させていただきます!

手を動かす部分はChatGPTに壁打ちしながら、理解していきました。その手順も記載しておりますので、ぜひ、私と同じ初心者の方はご覧いただけますと幸いです!

目次

GraphQLとは?

GraphQLとは、APIのクエリ言語およびランタイムで、必要なデータを柔軟にリクエストし、取得できる仕組みです。

通常APIとしてよく使われるREST APIは、エンドポイントごとに決まったデータがまとまって送られてきますが、GraphQLでは一つのエンドポイントに対して、クライアントが「この部分のデータだけください」と細かく指定できます。

より具体的な説明を下記にて記載します。

REST API

  • 必要なデータに応じて、複数のエンドポイントを呼び出す必要がある。
    • 例)
      • /users → ユーザー情報を取得
      • /posts → 投稿情報を取得
      • /comments → コメント情報を取得
  • データ取得時に不要なデータも含まれるため、レスポンスのデータ量が増える。
    • 例) ユーザーID 1のデータを取得する(/users?id=1)ときのレスポンス
      • {
          id: 1
          title: 今日の投稿
          post: 初めましてベンジャミンの木村です…
        }

GraphQL

  • すべてのリクエストを1つのエンドポイント(/graphql)で処理する。
  • 必要なデータだけを取得できる。
    • 例)ユーザーID 1の投稿タイトルだけを下記クエリで取得可能です。
      • query {
        user(id: 1) {
        posts {
        title
        }
        }
        }

まとめると、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)を指定してサーバーを構築します。
  • 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);
      • 作成した newItemmenuItems という配列に追加します。
    • 3. 作成したメニューを返す
      •       return newMenuItem;
      • 新しく作成したメニュー(newItem)をクライアントに返します。

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には様々な構成要素がありますので、知りたい方は下記記事が大変わかりやすかったので、ご参照ください。

GraphQLの構文まとめてみた

https://qiita.com/h-i-ist/items/805ab88f0242740695ed

次はAppSyncについて記述したいと思います!乞うご期待!

Related posts