Node.js + ApolloServer + mongodb(mongoose)でリアルタイムなGraphQLサーバを構築する。

どうも。オンプレのインフラ企業からWeb系企業に転職し、4ヶ月のハヤシです。

最近は業務でgraphqlに触る機会があり、おもしろいと思ったので簡単なgraphqlサーバを構築します。
あまり詳しいことは書かず、とりあえず動く！を目標に書くので、よろしくお願いします。

# GraphQLについて
> GraphQL（グラフQL）は、APIのために作られた、データクエリとデータ操作のための言語と、保存されたデータに対してクエリを実行するランタイムである[2]。GraphQLは、2012年にFacebookの内部で開発され、2015年に公開された
ウェブAPIの開発に、RESTやその他のWebサービスと比較して、効率的、堅牢、フレキシブルなアプローチを提供する。GraphQLでは、クライアントが必要なデータの構造を定義することができ、サーバーからは定義したのと同じ構造のデータが返される。したがって、必要以上に大きなデータが返されるのを防ぐことができる。
※wikipediaより抜粋。

REST APIが今まで主流でしたがGraphQLはそれに代わるもので、近年人気が高まっているものらしいですね。

その中でGraphQLのsubscriptionは以下の様な特徴を持っています。websocketsを通してリアルタイム通信を実現しているようですね。
> GraphQLサブスクリプションは、サーバーからのリアルタイムメッセージをリッスンすることを選択したクライアントにサーバーからデータをプッシュする方法です。サブスクリプションは、クライアントに配信するフィールドのセットを指定するという点でクエリに似ていますが、サーバーで特定のイベントが発生するたびに、単一の回答をすぐに返す代わりに結果が送信されます。

# 今回作るもの
Message投稿できるmutationと投稿されたMessageを確認できるQuery、リアルタイムで情報を取得できるsubscriptionのクエリを付属しているPlayGroundで確認できるgraphqlサーバの作成を行います。また通常のqueryやmutationで使われるhttpServerとは別にsubscriptionはWebSocketで動作しますので、同じポートでサーバが立ち上がるように設定します。

今回の構成では基本的には以下の4つのファイルが1セットとなって動作します。

- server.js  
→ graphqlサーバを立ち上げるための記述を行います。
- resolvers.js  
→graphqlに来たクエリなどに対しての実際の処理を記述します。DBから情報をとってくる処理など。
- schema.graphql  
→typeDefsです。graphqlの型などを定義します。ここに定義したものだけがクエリとして受け取れます。
- models/Message.js  
→mongodbに接続するmongooseのスキーマを定義します。

nodeを使用するので予めnodeを使用するPCにインストールしておいて下さい。
v10.14.2で動作確認をしています。

## 完成品github
https://github.com/yukihayashi06/graphql-test

## 0.Package導入
- nodemon #自動でコードの変更を検知してサーバを再起動してくれる。
- graphql 
- apollo-server-express #expressというnodeのフレームワークをApolloServerで使えるようにしてくれたもの
- mongoose(mongodb用) mongodbとつなぐことができるormです。
- babel  nodejsでES5?の記述方法をすることができるツール

以下packege.jsonです。

```
{
  "name": "graphql",
  "version": "1.0.0",
  "main": "server.js",
  "scripts": {
    "server": "nodemon --exec babel-node server.js",
    "start": "babel-node server.js"
  },
  "keywords": [],
  "license": "ISC",
  "dependencies": {
    "apollo-server-express": "^2.9.7",
    "express": "^4.17.1",
    "graphql": "^0.13.2",
    "mongoose": "^5.2.6"
  },
  "devDependencies": {
    "@babel/core": "^7.7.2",
    "@babel/node": "^7.7.0",
    "@babel/preset-env": "^7.7.1",
    "concurrently": "^3.6.0",
    "nodemon": "^1.18.1"
  }
}
```

### Packageをインストールする。

package.jsonを任意のディレクトリに貼り付けて以下のコマンドを実行し、インストールします。

`npm install`

node.jsでimport文などを使用するため、以下のファイルを作成してください。

```js:.babelrc
{
  "presets": [
    "@babel/preset-env"
  ]
}
```

これで開発に必要な準備は整いました。

# 1. 必要なファイル群を作成する。
以下のファイル達をserver.jsがあるディレクトリに作成してください。
 
```graphql:typeDefs.graphql
type Message {
  content: String
}

type Subscription {
  messagesCreated: Message!
}

type Query {
  Messages: [Message]!
}

type Mutation{
  addMessage(content: String): Message!
}
```
実際の処理は書かず、このデータがほしい！といったリクエストに対し、このデータを返します。といった型みたいなものを定義します。実際のDB操作の処理はresoloversに書きます。

---

```js:models/Message.js
const mongoose = require("mongoose");

const MessageSchema = new mongoose.Schema({
  content: {
    type: String,
    required: true
  },
});
const Message = mongoose.model("Message", MessageSchema);

module.exports = { Message };
```
Messageモデルには内容を示すcontentのみ定義します。
DBのMessageスキーマを定義します。ここで定義したデータしかmongodbに入りません。

---

```js:resolvers.js
import { PubSub } from "apollo-server-express";
const pubsub = new PubSub();
const MESSAGE_CREATED = "MESSAGE_CREATED";

module.exports = {
  Query: {
    Messages: async (_, args, { Message }) => {
     //Messageのcontextからすべてのメッセージを降順で取得します。
      const messages = await Message.find({})
        .sort({ createdDate: "desc" })
      return messages;
    }
  },
  Mutation: {
    addMessage: async (_, { content }, { Message }) => {
　　　 //Messageのcontextのインスタンスを生成し、contentの内容をmongooseを通してmongodbに保存し、pubsubを使ってsubscriptionしています。これで新規メッセージを投稿する度にリアルタイムで通知が行われます。
      const newMessage = await new Message({
        content
      }).save();
      await pubsub.publish(MESSAGE_CREATED, { messagesCreated: newMessage }); //ここでsubscriptionに通知をしています。
      return newMessage;
    }
  },
  //ここでpublishされたメッセージに対してasyncIteratorを使用して非同期でくるデータに対して反復処理をしています。
  Subscription: {
    messagesCreated: {
      subscribe: () => pubsub.asyncIterator([MESSAGE_CREATED])
    }
  }
};

```
このファイルはqueryやmutationをうけての実際にDBにデータを操作する処理を記述します。
PubSubをimportし、リアルタイムでデータを受け取ることができます。
Messages、addMessageはtypeDefsで定義したMessages、addMessageと対応しています。

各メソッドの引数については以下のようになっています。基本的にはargsと、contextとresultのみ実装を意識すれば大丈夫です。引数には順番があるので、注意が必要です。
`fieldName(obj, args, context, info) { result }`
くわしくは以下を参照ください。
https://www.apollographql.com/docs/graphql-tools/resolvers/

---

最後に上記のファイル達インポートした以下のファイルを作成してください。

```js:server.js
const fs = require("fs");
const path = require("path");

//apollo関連
import { ApolloServer } from "apollo-server-express";
const express = require("express");
import { createServer } from "http";

//schema,resolver関連
import resolvers from "./resolvers.js";
const filepath = path.join(__dirname, "typeDefs.graphql");
const typeDefs = fs.readFileSync(filepath, "utf-8");

//model
const { Message } = require("./models/Message");

// MONGOOSE接続部分
const mongoose = require("mongoose");

//conncectに書いてあるURLは適宜変更してください。また、通常はenvファイルなどで管理するようにして下さい。
mongoose
  .connect(
    "mongodb+srv://test:***********@cluster0-9v8cy.mongodb.net/graphql-test?retryWrites=true&w=majority",
    {
      useNewUrlParser: true,
      useFindAndModify: false
    }
  )
  .then(() => console.log("DB connected"))
  .catch(err => console.error(err));

//サーバ立ち上げ
const server = new ApolloServer({
//ここで上記で作ったファイルを含めています。
  typeDefs,
  resolvers,
//ここでreturnされたcontextをresolversの第三引数で使用する事ができます。
  context: async ({ req, connection }) => {
    return { Message };
  },
  playground: true,
  introspection: true,
});

const app = express();
server.applyMiddleware({ app, path: "/graphql" });

//subscription(リアルタイム通信設定部分)
const httpServer = createServer(app);
server.installSubscriptionHandlers(httpServer);

const PORT = 4000;

httpServer.listen(PORT, () => {
  console.log(
    `🚀 Server ready at http://localhost:${PORT}${server.graphqlPath}`
  );
  console.log(
    `🚀 Subscriptions ready at ws://localhost:${PORT}${server.subscriptionsPath}`
  );
});
```

### mongooseはmongodbAtlasを使用しているので、以下のサイトから登録して「mongoose.connect()」の部分に接続URLを記述してください。
https://www.mongodb.com/cloud/atlas

---

ここまでの手順でサーバが動作する準備が整いましたので実際に動作させてみます。

# 2.サーバ立ち上げ
server.jsが配置されているディレクトリで以下のコマンドを使用しサーバを立ち上げてください。

```npm run start ```

以下のような表示が出ていればサーバの立ち上げには成功しています。

```
🚀 Server ready at http://localhost:4000/graphql
🚀 Subscriptions ready at ws://localhost:4000/graphql
(node:88080) DeprecationWarning: current Server Discovery and Monitoring engine is deprecated, and will be removed in a future version. To use the new Server Discover and Monitoring engine, pass option { useUnifiedTopology: true } to the MongoClient constructor.
DB connected
```

# 3.動作確認

実際に以下のURLにアクセスし、playGround上で動作を確認してみます。
http://localhost:4000/graphql

以下のような画面が表示されていると思います。ここで実際にクエリなどを叩いて動作を確認していきます。
<img width="1680" alt="スクリーンショット 2019-11-20 22.24.32.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/322330/55eae33d-62be-21c9-49e4-b2ed23207fdd.png">

右側の部分にMessageCreatedにsubscriotionのクエリを作成し、画面中央の▶ボタンを押します。
<img width="1680" alt="スクリーンショット 2019-11-20 22.26.56.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/322330/b554f6f2-de0d-4536-ddc1-77410cc1a6e7.png">

画面の表示が代わり右下にlisteningとなり、Messageの投稿を監視しつづけます。
<img width="1680" alt="スクリーンショット 2019-11-20 22.29.18.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/322330/89db3320-0286-e13a-c1f9-0f594a15c60f.png">

これでリアルタイムにデータの更新通知を受け取る事ができます。実際にMessageを投稿するmutationを叩いてみます。
右側の部分に以下の様に入力し画面中央の▶ボタンを押します。
<img width="1680" alt="スクリーンショット 2019-11-20 22.41.59.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/322330/cc033aa4-b93c-3c0e-9e75-cc12b3b085ab.png">

右側に以下のように表示されたらMessageの投稿に成功しています。
<img width="1680" alt="スクリーンショット 2019-11-20 22.42.42.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/322330/9f0beca0-e911-8eb4-c99a-bc935e5e451e.png">

MessageCreatedに戻ると以下のように右側にaddMessageで追加したテキストが表示されている事がわかります。
複数ブラウザを立ち上げてもリアルタイムに更新されると思います。あとはフロント側でこの更新された通知を受け取るなどすることができます。

<img width="1680" alt="スクリーンショット 2019-11-20 22.44.36.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/322330/321bedee-f449-bbb7-aadd-0ec486880d4b.png">

かなり駆け足でしたが、これでリアルタイムに更新されるgraphqlサーバを構築する事ができました。
次回は、フロントで作ったgraphqlサーバと接続して簡易的なチャットアプリを作成したいと思います。

ここまで読んでくださりありがとうございました。
何かおかしな点があれば修正いたしますので、コメントください。