GraphQL を RESTful API と比較しながら実装して理解する

この記事は GraphQL Advent Calendar 2018JX通信社 Advent Calendar 2018の19日目です。

NewsDigest で API のバックエンドとして GraphQL を本番利用して1年ぐらい経ちました。GraphQL 自体は、「クエリ言語」という位置づけですが、実際には「API のスキーマ」という使われ方(a.k.a. Anti-REST な何か)が多いと思います。

f:id:yamitzky:20181215174940p:plain

実際、NewsDigest の API としては、もともと RESTful なものを運用してたのですが、GraphQL へ移行しました。つまり、

  • RESTful の代わりとしての利用
  • プライベートな API
  • アプリから使う API
  • サーバーサイドエンジニアがメンテするもの(BFFな文脈で、フロント側のエンジニアがメンテするケースも多いと思いますが)

といった使い方です。GitHub のようなパブリックな API でもなければ、Facebook のようにリソースが複雑に絡まりあっているようなものでもなく、ただシンプルに「アプリ用の、RESTful API の代わり」として使っている感じです。

さて、GraphQL が流行ってくるにつれて「GraphQL は何をやってくれるものなの?」「どう実装したら良いの?」という話が聞かれたりしたので、「REST と比較して実装みたらわかりやすいんじゃないか」という趣旨の記事です。

RESTful vs GraphQL

まず、RESTful な API では、次のような URL が生えてきます。

  • GET /books
  • GET /books/1
  • POST /books
  • PUT /books/1
  • DELETE /books/1

一方で GraphQL な API では、最低限必要なのは次の URL だけです。 *1

  • POST /graphql

そのかわりに、POST のペイロードとして次のようなものを送ります。REST における GET /books は、

{
    books {
        author
        name
    }
}

といった具合です。 *2

RESTful な API

今回は簡略化して、 GET /booksPOST /books だけを扱います。

例として、Node の express で実装してみます。

const express = require('express')
const bodyParser = require('body-parser')
const app = express()
app.use(bodyParser.json())

const BooksDB = {
  db: [{ author: "yamitzky", name: "Road to GraphQL" }],
  list () {
    return this.db
  },
  add (book) {
    this.db = [...this.db, book]
  }
}

app.get('/books', (req, res) => {
  const books = BooksDB.list()
  res.json({ books })
})

app.post('/books', (req, res) => {
  const book = req.body
  BooksDB.add(book)
  res.json(book)
})

app.listen(3000)

実装としては単純で、GET /booksPOST /books というエンドポイントに、それぞれ「本の一覧を取得」と「本を追加」という処理を書いたハンドラー(コールバック)を追加しているだけです。

これは express 固有の話じゃなくて、他の API 用フレームワーク等(例えば Flask や Go の net/http) でも「パスを定義して、それを処理するハンドラーを書く」というのはだいたい同じでしょう。Rails のような MVC な設計であれば、Controller の Action を生やしていくイメージです*3

GraphQL な API

次に、 apollo というライブラリを使って、GraphQL な API を実装してみます。まずは、 GET /books に該当するものから実装してみましょう。

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

const BooksDB = {
  db: [{ author: "yamitzky", name: "Road to GraphQL" }],
  list () {
    return this.db
  },
  add (book) {
    this.db = [...this.db, book]
  }
}

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

  type Query {
    books: [Book]
  }
`

const resolvers = {
  Query: {
    books () {
      return BooksDB.list()
    }
  },
}

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

先程の REST な API と比較して新しく出てきた概念は、 typeDefsresolvers を定義しているところです。前者は、GraphQL のスキーマを定義していて、後者は、スキーマ上に定義されたリソースをどのように解決するか(要はDBから取ってきてオブジェクトを返すもの)を定義したリゾルバーです。この「スキーマ定義」と「リゾルバー」に出てくる Querybooks といった変数が、ちょうど、対応しています。

これはまさに、「URL(パス)の定義」と「ハンドラー」の対応関係と一緒です。RESTful な API においては、URL とリソースは対応しているわけですから、REST と GraphQL の違いは、 「リソースをどのような URL で定義するか」と「リソースをどのようなスキーマで定義するか」という違い になります。

今度は、POST /books も追加してみましょう。GraphQL での Create/Update/Delete の操作は、各操作を関数っぽく定義していきます。*4

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

  type Query {
    books: [Book]
  }

  type Mutation {
    addBook(author: String!, name: String!): Book!
  }
`

const resolvers = {
  Query: {
    books () {
      return BooksDB.list()
    }
  },
  Mutation: {
    addBook (root, args) {
      const book = args
      BooksDB.add(book)
      return book
    }
  }
}

Mutation(Create/Update/Delete)に関しても、Query と同じように、フィールドとリゾルバーを定義します。

今回は apollo というライブラリの例ではありますが、Python の graphene や Go の gqlgen などでも全く同じで、「スキーマにフィールドを定義して、それに対応したリゾルバーを用意する」という形になります(graphene はスキーマを class で定義する、という違いはあります)。

GraphQL が提供しているもの

GraphQL 自体は冒頭にも述べたように、ただの「クエリ言語」です。なので、その 実装や、どういうデータベースをバックエンドに持つか、あるいは、HTTPで提供するかどうかさえも関心外 です。

BFF の文脈では、GraphQL な API が提供するのは「秩序ある神エンドポイント /graphql」です。「1リクエストで、必要なリソースだけをすべて取得する」という手段が提供することによって、無秩序にフロントエンド向け URL を生やすということがなくなるわけです。

また、Prisma のようなものもあります。こういうのは、「レールに乗っけると便利になる、フルスタックフレームワーク」みたいな感じで認識しておくと良いと思います(使ったことないけど)。すでにあるデータベースなどの資産を活かして移行したいような場合とかは、apollo のような薄いものを使うと良いと思います。

GraphQL を使うべきか?

GraphQL は、秩序を守りながら API を提供する手段に過ぎないです。 なので絶対に GraphQL を使うべき理由も、絶対に GraphQL を使うべきでない理由も、あんまりない ような気がしています。

けど、便利なケース/便利じゃないケースはあります。便利じゃないケースとしては、ファイルアップロード(multipart form)とかは一工夫必要です。ちなみに、 NewsDigestでの「REST API の代わりとして使う」は普通に便利なケース でした。

あとは、Prisma や AppSync*5apollo-link-state *6 のような、GraphQL のルールに則ることで便利に使えるものも多いので、活用すると DX が上がると思います*7

メリットに関する補足 ↓

yamitzky.hatenablog.com

まとめ

まとめると、 RESTful な API の「URL を定義して、対応するハンドラーを実装する」はGraphQL における「スキーマを定義して、対応するリゾルバーを実装する」に該当する ということです。

また、JX通信社では、一緒に GraphQL API を作りたい仲間を募集中です。ちなみに GraphQL API は Go 使ってます。

www.wantedly.com

*1:実際には GET でも取得できるケースも多いと思います。また、どういう URL かは、GraphQL の責務ではありません。例えば、 PUT /hogehoge というエンドポイントだけを生やして、GraphQL API を作ることもできます

*2:イメージをつきやすくするために、かなり簡略化した例です

*3:Rails 久しく触ってないので、誤りがあればすみません

*4:こちらの例では Book 型で返してますが、成功したか失敗したかだけを Bool で返しても特に問題ないです

*5:マネージドな GraphQL APIサーバーです

*6:Redux のような状態管理のライブラリで、フロントエンドの状態管理を GraphQL のスキーマに乗っけることができます

*7:NewsDigestではいずれも使ってないのですが・・・笑