beta

GraphQLのAPIをNode.js + Express + MySQLで試してみた

APIのフォーマットとしてRESTに置き換わると言われるGraphQLを、Node.jsのExpressサーバーでMySQLデータベースと連携させて試してみました。サーバーを立てて、HTMLページからアクセスしてMySQLからデータを取ったり更新したりするところまでやっていきます。

公開日:2019年11月28日

やって見たこと

とりあえずお試しなので、

  • 一覧を返す
  • データを挿入・更新する

というところだけ試してみました。

ネット上でよく見るGraphQLの記事は、内部的に変数を持って表示・更新するパターンが多くて、実際にMySQLデータベースとの連携について書いてある記事が少なかったので、手探りで試しながらやって見ました。

テストAPIの仕様

  • 「members(name, genderの2カラムだけ)」テーブルに対して、取得と挿入・更新をする

ということだけです。

データは、

name gender
hanako Women

こんなデータが入っていることとします。

ファイル構造

├── config/db.js
├── index.js
├── schema.js
├── root.js
├── node_modules
└── package.json

こんな形になっています。

GraphQL + MySQL APIサーバーを実装する

ここからは、各ファイルの中身を見ながら、実際に処理内容やルールを確認しつつ、GraphQL + MySQL APIサーバーを実装していきます。

package.json

{
  "name": "graphql-api-test",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "license": "MIT",
  "dependencies": {
    "cors": "^2.8.5",
    "express": "^4.17.1",
    "express-graphql": "^0.8.0",
    "graphql": "^14.4.0",
    "lodash": "^4.17.15",
    "mysql": "^2.17.1"
  },
  "devDependencies": {},
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "node ."
  },
  "author": "koji kadoma"
}

node.jsの管理ファイルです。

express周りと、graphql、mysql、corsモジュールを追加しています。

このファイルがあるディレクトリで、コンソールから、

npm install
もしくは
yarn install

を実行すると、node_modulesディレクトリに必要ファイルがインストールされます。

index.js (expressサーバー)

const express = require('express')
const graphqlHTTP = require('express-graphql')
const cors = require('cors')

// database
const schema = require('./schema')
const root = require('./root')

// server
const port = process.env.PORT || 8080
const app = express()
app.use(cors())
app.use(express.static('./'));
app.use('/', graphqlHTTP({
  schema: schema,
  rootValue: root,
  graphiql: true
}))

app.listen(port, err => {
    if (err) throw err
    console.log(`> Ready On Server http://localhost:${port}`)
});

サーバーの部分です。

ほとんど、ただのExpressサーバーです。express-graphqlモジュールとAPIなのでCORS対策のためのcorsモジュールを追加しています。

通常のExpressサーバーと違うのは、HTTPリクエストをexpress-graphqlモジュールで処理しているところです。

app.use('/', graphqlHTTP({
  schema: schema,
  rootValue: root,
  graphiql: true
}))

パラメータの「graphiql」は、GUIのGraphQL Playgroundを起動するかの値です。一般公開するときには、falseにした方が良いでしょう。

schema.js (GraphQLのデータ構造を定義する)

const { buildSchema } = require('graphql')

// type メソッド名 (検索キー): [返答キー]

const schema = buildSchema(`
  type Member {
    name: String
    gender: String
  }

  type Query {
    members(
      first: Int,
      name: String,
      gender: String
    ): [Member],
  }

  type Mutation {
    addMember(name: String!, gender: String! ): Member
  }
`)

module.exports = schema;

GraphQLの核になるところです。

ブラウザなどから来るリクエストを、どのようなフォーマットで受け取るかなど、GraphQLの構造を定義します。ここが一番理解しづらいところです。

書き方のフォーマット

まず、書き方は、

type メソッド名 (検索キー): [返答キー]

というフォーマットで書きます。検索キーは省略可能ですが、返答キーは挿入・更新の時(mutation)でも必須です。ないとエラーになります。

また、分かりずらいのは、typeの後のメソッド名の書き方ですが、

  • 取得は「Query」の下にカンマ区切りで書く
  • 更新・挿入は「Mutation」の下にカンマ区切りで書く
  • それ以外は、自分の独自の定義なので自由

というルールがあります。

つまり、「Query」「Mutation」はGraphQLの予約されています、それ以外は自分オリジナル定義で(最初の「type Member」など)として使えます

全部が一緒になっていて混乱しがちですが、「Query、Mutationがあれば良い」ということを覚えておくと良いでしょう。

リクエストと定義の紐付け

ブラウザなどからクエリ(schema.jsの「type Query」とは違います。リクエストクエリです)が投げられる際にどのように定義と紐付けられるかというと、

  • 無記名クエリは常に「取得(Query)」として処理される
  • 更新・挿入系クエリは「mutation」と宣言されたもののみ処理される

というルールになっています。

具体的には、

{
  member {
    name
    gender
  }
}

とすると、schema.jsの「type Query」以下の定義から、「member」という定義を探しに行きます。

更新・挿入系は、

mutation{
  addMember(
    name: "Tarou"
    gender: "man"
  ){
    name
    gender
  }
}

と、クエリの最初に「mutation」と宣言することで、schema.jsの「type Mutation」以下の定義から、「addMember」という定義を探しに行きます。

オリジナル定義の使い方

オリジナル定義は、各定義の「検索キー」「返答キー」に使うことができます。

例えば、Queryのmemberの部分の返答キーで指定している「[Member]」は、先に定義している「type Member」を指しています。

members(
  first: Int,
  name: String,
  gender: String
): [Member],  // <- ここは type Memberを引いている

このように、先にフォーマットを定義しておくことで、同じようなクエリを定義する度に、検索キーや返答キーを書かないで済むため、コードがスッキリしスリムにできます。

毎回書けばいいじゃないか?と思うところですが、今回のテストのような小規模では問題ないですが、APIとして規模が大きくなった時に恩恵を受けられる部分です。

root.js (実際のデータの操作を書く)

schemaで定義したデータを実際にデータベースに書き込むところです。ここの情報が少なくて難儀しました。

const connectDB = require('../config/db')

const root = {
  members: (search) => {
    return new Promise((resolve) => {
      let conditions = 'SELECT * FROM `members`'
      const filter = Object.keys(search)
      if(filter.length !== 0) {
        conditions += ' WHERE `' + filter[0] + '` = "' + search[filter[0]] + '"'
      }

      connectDB.excute(conditions).then((results) => {
        resolve(results)
      })
    })
  },
  addMember: (search) => {
    return new Promise((resolve) => {
      let conditions = 'INSERT INTO `members` (name, gender) VALUES (\'' + search.name + '\', \'' + search.gender + '\') ON DUPLICATE KEY UPDATE name = VALUES(name)'

      connectDB.excute(conditions).then((results) => {
        resolve(results)
      })
    })
  },
}

module.exports = root;

rootの書き方は

  • root直下のメソッド名は、GraphQLのクエリ名に合わせる(それぞれ、type Query、type Mutation以下のクエリ名)
  • 処理をしたら、結果を返す

となっています。

とてもシンプルなものですが、「root直下のメソッド名は、GraphQLのクエリ名に合わせる」というのがミソです。

つまり、

  1. ブラウザなどからGraphQLフォーマットのクエリを受け取る
  2. schema.jsで該当するクエリを探す
  3. root.jsからクエリ名のメソッドを探して、パラメータ(ここでは変数「search」)を渡して実行

という流れになっています。root.jsのメソッドに、GraphQLのクエリ名と同じものがない場合はエラーになります。

ここまで見てお気づきかと思いますが、いかにGraphQLが汎用性の高い仕組みだと言っても、実際にMySQLのデータにアクセスする際は普通にSQL文で処理しているということです。

ここを簡略化するためには、ORMマッパーなどを使うことになりますが、そうすると、特に書き込み系の場合、GraphQLを使うよりもORMマッパーを直で叩く仕組みにした方がシンプルかもしれません。

config/db.js

module.exports = {
  excute: function (conditions) {
    return new Promise ((resolve, reject) => {
      const mysql = require('mysql')
      const connection = mysql.createConnection({
        host: 'localhost',
        port: 3306,
        user: 'root',
        password: 'root',
        database: 'members'
      })
      connection.connect((err) => {
        if (err) {
          console.error('error connecting: ' + err.stack)
          resolve()
        }

        connection.query(conditions, (err, results, fields) => {
          resolve(JSON.parse(JSON.stringify(results)))
        })
      })
    })
  }
}

ここは、root.jsで読み込んでいるDB設定と実行の部分です。

host: 'localhost',
port: 3306,
user: 'root',
password: 'root',
database: 'members'

の部分を変更すればOKです。

これでAPI部分は完成です。

node index.js

で、サーバーを起動させれば、GraphQL Playgroundでテストができます。

HTMLでデータを取得するサンプル

最後に、ページ側からGraphQL APIにアクセスしてデータを取得する部分を試してみます。

member.html

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
</head>
<body>
<h1>メンバー一覧</h1>
<button onclick="searchMember()">表示</button>
<ul id="memberlist"></ul>

<h2>追加</h2>
<input type="text" name="" id="name" placeholder="名前">
<select name="" id="gender">
  <option value="-">-</option>
  <option value="man">man</option>
  <option value="woman">woman</option>
</select>
<button onclick="addMember()">追加</button>


  <script>
    const addMember = () => {
      const gender = document.getElementById('gender').value
      const name = document.getElementById('name').value
      if(name && gender !== '-') {
        data = `
          mutation{
            addMember(
              name: "${name}"
              gender: "${gender}"
            ){
              name
              gender
            }
          }
        `
        let url = 'http://localhost:8080/?query='
        fetch(url + encodeURIComponent(data), {
          method: 'POST',
        }).then(response => response.json())
        .then(result => {
        })
      }
    }
    const searchMember = () => {
      let data;
      data = `
        {
          members {
            name
            gender
          }
        }
      `

      let url = 'http://localhost:8080/?query='
      fetch(url + encodeURIComponent(data), {
        method: 'POST',
      }).then(response => response.json())
      .then(result => {
        const memberslist = document.getElementById('memberlist')
        memberslist.textContent = null;

        const members = Array.from(result.data[path])
        members.map((member) => {
          const li = document.createElement('li')
          const items = Object.keys(member)
          items.map((item) => {
            li.innerText += `${item}: ${member[item]}, `
          })
          memberslist.appendChild(li)
        })
      })
    }

  </script>
</body>
</html>

長いですが、大事なのは、

let data;
data = `
  {
    members {
      name
      gender
    }
  }
`

let url = 'http://localhost:8080/?query='
fetch(url + encodeURIComponent(data), {
  method: 'POST',
}).then(response => response.json())
.
.
.

この部分です。

GraphQL形式の文字列をencodeURIComponentでエスケープしてURLにしています。

また、先ほど述べたとおり、更新・挿入系の時は、データに「mutation」が必要です。

data = `
   mutation{ // <-- ここが必要
     addMember(
       name: "${name}"
       gender: "${gender}"
     ){
       name
       gender
     }
   }
 `

これで、一覧の取得や新規データの送信ができるようになりました。

リクエストの方法

上記のJavascriptでは、一応POSTでリクエストをしていますが、URLにパラメータが載っています。一応、公式ドキュメント上はこれで正しいようですが、URLに表示しないでPOSTだけにすることも可能です。

graphql/express-graphql

その際は、fetchのリクエストヘッダにデータを入れ込みます。

let url = 'http://localhost:8080/'
fetch(url, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({query: data})
}).then(response => response.json())

JSON化する部分は、必ず「{query:}」という連想配列にする必要があります。


わざわざいちから書かなくても、HasuraやPrismaなどのフレームワークを使えばもっと簡単にGraphQLサーバーを作れますが、一応仕組みを理解する意味でも一度自分で手書きでやってみてました。

感覚的にはREST APIよりも楽で汎用的に作れる気がしました。

今回はテスト的な感じでしたが、今後より実践的なものを作ってみて、ノートにしたいと思います。