ApolloServerでGraphQLを体験する


はじめに

この記事は2020年のRevCommアドベントカレンダー5日目の記事です。4日目はmiyさんの「AGI(Asterisk Gateway Interface)を使って内線電話からPythonスクリプトを実行する」でした。

こんにちは。RevCommでエンジニアをしている酒井です。

業務ではWebアプリケーションのフロントエンドバックエンド両方担当しており、よく触るのはnodejs、たまにpythonといった感じです。
業務でGraphQLを扱っており「便利だなーすごいなー他の人にも触って欲しいなー」と思ったのですが、とりあえず内容読んでコピペして動くようなチュートリアルが少ないなと感じたので書いてみます。

とりあえずやってみましょう

「とりあえず動くぜすごい!」を目標にしているので詳しい説明はすっ飛ばして進みます。「ここどう動いてるの?」「こういうことできないの?」と疑問が多々出てくると思いますが、そういった物は公式ページを参照してください。

サーバー構築〜query実行まで

この項はほぼ公式のチュートリアルと同じ流れですのでそちらも参照ください。

適当なディレクトリを作成してから

npm init --yes
npm install apollo-server graphql
touch index.js

作成されたindex.jsに以下を貼り付け

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

// GraphQLスキーマの定義
const typeDefs = gql`
  type Book {
    title: String
    author: String
  }

  type Query {
    books: [Book]
  }
`;

// queryで取得するデータ
const books = [
    {
      title: 'The Awakening',
      author: 'Kate Chopin',
    },
    {
      title: 'City of Glass',
      author: 'Paul Auster',
    },
  ];

// フィールドのデータを返す関数(リゾルバ)
const resolvers = {
    Query: {
      books: () => books,
    },
  };

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

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

これでもう動くようにはなっています。

node index.js

http://localhost:4000/にアクセスすると以下の画面が表示されるはずです。

これはApolloServerにホストされたプレイグラウンドです。ここからクエリを発行して結果を受け取ることができます。入力補完が効いたり、自動でqueryを書き起こしておいてくれたりとなかなか便利です。(もちろん無効にしてプレイグラウンドにアクセスできないようにすることもできます)

左側にqueryを書いて、真ん中の再生マークをクリックすれば右側に結果が表示されます。簡単です。
試しに以下を左側に入力してみましょう

query {
  books {
    title
    author
  }
}

二冊の本の情報が返ってきたはずです。
queryはRESTAPIでいうとGETにあたります。ここでは全ての本の情報を取得する処理になっていますが、queryに引数を入れることもできます。

mutation

データの取得をできたので次は更新です。GraphQLではデータ更新系の物はmutationに記述します。createもupdateもdeleteも全部mutationです。
この項では本の追加と削除の処理を追加していきます。
(ちなみにここからは公式のチュートリアルにはない内容になってきます)

まずGraphQLスキーマを次のように書き換えます

const typeDefs = gql`
  type Book {
    title: String
    author: String
  }

  type Query {
    books: [Book]
  }

  type Mutation {
    addBook(title: String!, author: String): Book
    deleteBook(title: String!): Boolean
  }
`;

addBookdeleteBookが追加されました。
次にリゾルバを書き換えます。

const resolvers = {
  Query: {
    books: () => books,
  },
  Mutation: {
    addBook: (parent, args, context, info) => {
      books.push(args);
      return args;
    },
    deleteBook: (parent, {title}, context, info) => {
      const bookIndex = books.findIndex(item => item.title == title);
      if (bookIndex == -1) {
        throw new ApolloError('book not found', 'NOT_FOUND');
      }
      books.splice(bookIndex, 1);
      return true;
    }
  },
};

最後に最初の行を変更します。

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

これで本の追加と削除ができるようになっているはずです。
以下をプレイグラウンドに入力してみましょう。

mutation {
  addBook(title: "hoge", author: "hogehoge") {
    title
    author
  }
}

追加した本の情報が返ってきたかと思います。
次に前の項で使った本を取得するqueryを実行してみてください。返ってくる本の情報が三冊に増えているはずです。

追加ができたので削除をしてみましょう。

mutation {
  deleteBook(title: "City of Glass")
}

実行後、再度本を取得すると、"City of Glass"が削除されていることが確認できると思います。
(存在しない本を削除しようとするとエラーが返ってくるはずです)

subscription

subscriptionは簡単にいうと変更検知の機能です。今まで使っていた例でいうと、mutationで本が追加されたり削除されたりあるいは更新されたのをリアルタイムに知ることができます。とても便利。いちいちqueryせずにすみますやったね。
とっても便利なのですが、queryやmutationと少し書き方違うのでハマりどころだったりします。ここでは出来上がったコードをお見せしますが、もし皆さんがご自分で勉強するときはしっかりとドキュメント読み込むことをお勧めします。

今回は書き換わっている箇所が多いので一気にドンといっちゃいます。

index.js
const { ApolloServer, ApolloError, PubSub, gql } = require('apollo-server');

const pubsub = new PubSub();

const BOOK_ADDED = 'BOOK_ADDED';

// GraphQLスキーマの定義
const typeDefs = gql`
  type Book {
    title: String
    author: String
  }

  type Query {
    books: [Book]
  }

  type Mutation {
    addBook(title: String!, author: String): Book
    deleteBook(title: String!): Boolean
  }

  type Subscription {
    bookAdded: Book
  }
`;

// queryで取得するデータ
const books = [
    {
      title: 'The Awakening',
      author: 'Kate Chopin',
    },
    {
      title: 'City of Glass',
      author: 'Paul Auster',
    },
  ];

// フィールドのデータを返す関数(リゾルバ)
const resolvers = {
  Query: {
    books: () => books,
  },
  Mutation: {
    addBook: (parent, args, context, info) => {
      books.push(args);
      pubsub.publish(BOOK_ADDED, { bookAdded: args });
      return args;
    },
    deleteBook: (parent, {title}, context, info) => {
      const bookIndex = books.findIndex(item => item.title == title);
      if (bookIndex == -1) {
        throw new ApolloError('book not found', 'NOT_FOUND');
      }
      books.splice(bookIndex, 1);
      return true;
    }
  },
  Subscription: {
    bookAdded: {
      subscribe: () => pubsub.asyncIterator([BOOK_ADDED]),
    }
  }
};

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

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

(mutationのaddBookにも変更が加わっているのでご注意を)

新しく以下のqueryを実行してください

subscription {
  bookAdded{
    title
    author
  }
}

右下の方にListening...と表示されたかと思います。
この状態でaddBookで本を追加し、subscriptionに戻ってみてください。追加した本の情報が表示されているはずです。別タブで同時に見るとわかりますが、addBookを実行すると即時にsubscription側に反映されています。

最後に

GraphQLを初めて触った時の「これすごい便利じゃん」を体験してもらうためにこの記事書いてみました。
需要(とやる気と時間)があれば続きで詳しい解説やクライアント側のことを書くかもしれません。
読んでくださった方ありがとうございました。

明日はsohichiroさんの記事です。