JavaScript
reactjs
gRPC
GraphQL
apollo

Apolloでの綺麗なAPI実装(GraphQL)を試す

はじめに

今回のサンプルは以下に用意しました。
Apollo Server & Apollo Clientサンプル

以前こんな記事を書きました。
React(+Redux)+gRPCで実現するクリーンアーキテクチャ+マイクロサービス構成
BFFのフロントエンドAPI部分に関して、次の記事を見てクライアント通信の部分をGraphQLで実装できるとより柔軟で堅牢な気がしたので試してみました。
世のフロントエンドエンジニアにApollo Clientを布教したい

REST API、 GraphQLの違いに関しては下記を参考にしてください
アプリ開発の流れを変える「GraphQL」はRESTとどう違うのか比較してみた

GraphQLを導入するメリットはざっくり以下の通りです。

  • APIのインタフェース定義(送信パラメータのデータ型定義)ができる(それにともない、データの型定義によるAPIパラメータのデータ型バリデーションチェックができる)
  • データ取得条件に合わせてGETエンドポイントを大量に作成しなくて済む(GraphQLスキーマ単位になる)
  • APIのバージョン管理ができる
  • APIレスポンスのキャッシュが容易

反面デメリットとしては以下があります。

  • 日本語の記事が少ない
  • GraphQLインタフェース定義を学習するコストがかかる

Apollo Clientに関しては通信後のデータ管理をLinkという機能で保持する仕組みも持っているため、Reduxの代替としても期待されています。

ApolloでのGraphQL導入

ApolloはGraphQLのフロントエンド&バックエンドのライブラリです。
バックエンド側はApollo Server、フロントエンド側は Apollo Clientを導入する必要があります。
またGraphiQLというVisual Editorがツールとして付属しているのでAPIの動作確認を簡単に行うことができます。

バックエンド(Apollo Server)

各種NodeJSフレームワークに対応しています。

  • express
  • koa
  • hapi
  • restify
  • lambda
  • micro
  • azure-functions
  • adonis

今回はexpressで実装するのでexpress対応のapolloをnpmインストールします。

npm install --save apollo-server-express graphql-tools graphql express body-parser

次の実装が最小サンプルとなります。

server.js
const express = require('express')
const bodyParser = require('body-parser')
const { graphqlExpress, graphiqlExpress } = require('apollo-server-express')
const { makeExecutableSchema } = require('graphql-tools')
const app = express()

process.on('uncaughtException', (err) => console.error(err))
process.on('unhandledRejection', (err) => console.error(err))

app.use(bodyParser.urlencoded({extended: true}))
app.use(bodyParser.json())


// GraphQLスキーマ定義
const typeDefs = `
  """
  type Query (必須)
  """
  type Query { books: [Book] }

  """
  返却するデータ構造
  """
  type Book { title: String, author: String }
`

// ダミーデータ
const books = [
  {
    title: 'Harry Potter and the Sorcerer\'s stone',
    author: 'J.K. Rowling',
  },
  {
    title: 'Jurassic Park',
    author: 'Michael Crichton',
  },
]

// resolvers
const resolvers = {
  Query: { books: () => books },
}

// スキーマ生成
const schema = makeExecutableSchema({
  typeDefs,
  resolvers,
})

// GraphQLエンドポイント
app.use('/graphql', bodyParser.json(), graphqlExpress({ schema }))

// GraphiQL:GraphQLクエリのvisual editor
// TODO: 本番デプロイ時はアクセス出来ないようにする
app.use('/graphiql', graphiqlExpress({ endpointURL: '/graphql' }))

app.listen(5000, () => {
  console.log('Access to http://localhost:5000')
})

次コマンドで実行

$ node server.js

http://localhost:5000/graphiqlにアクセスすると
GraphiQLが表示されます。
次のようにクエリを書いて▶ボタンで実行するとダミーデータが取得できます。

query {
  books {
      title,
      author,
  }
}

スクリーンショット 2018-06-10 15.07.16.png

GraphQLのスキーマ定義

細かい詳細は下記記事がまとまっています。
GraphQL入門 - 使いたくなるGraphQL

データの取得にはQuery、データの更新にはMutationを使います。
スキーマ定義にtype Queryもしくはtype Mutationの定義は必須です。
スキーマ定義は次のように行います。

server.js
const typeDefs = `

  type Query {
    フィールド名(引数): 返却データ型
  }

  type Mutation {
    フィールド名(引数): 返却データ型
  }

}
`

// resolvers
const resolvers = {
  Query: {
    フィールド名: (引数) => 返却データ,
  },
  Mutation: {
    フィールド名: (引数) => 返却データ,
  },
}

データの基本型は次のようになっています。

  • Int: 32bit整数型
  • Float: 浮動小数型
  • String: UTF-8 文字列型
  • Boolean: true もしくは false
  • ID: ユニークなスカラー値、キャッシュに使われる。Stringをシリアライズ(直列化)したデータで保存されている

加えて任意のオブジェクト単位にデータ型を定義できます。
次の例はBook型を定義した例です。
なお、パラメータを必須にしたい場合はデータ型末尾に!をつけます。

server.js
type Query { 
  books: [Book]!,
}

type Book { title: String, author: String }

さらに詳細はGraphQL公式:Schemas and Typesを参考にしてください。

複数メソッドを定義するときはtype Queryもしくはtype Mutationのブロック内にメソッドを追加します。
ちなみに"""で囲めばコメントになります。

server.js
const typeDefs = `
  type Query { 
    books: [Book],
    items: [Item],
  }

  """
  返却するデータ構造
  """
  type Book { title: String, author: String }
  type Item { title: String }
`

// resolvers
const resolvers = {
  Query: {
    books: () => books,
    items: () => items,
  },
}

引数を取る場合はつぎのように書きます。
Resolver function signature
argsにパラメータが渡ってきます。

server.js
// GraphQLスキーマ定義
const typeDefs = `
  type Query { 
    books(author: String): [Book],
  }

  type Book { title: String, author: String }
`

// resolvers
const resolvers = {
  Query: {
    books: (obj, args, context, info) => {
      const author = args.author
      return books.filter(book => book.author === author)
    },
  },
}

GraphiQLにて次のようなクエリを作成します。

query ($author: String!){
  books(author: $author) {
      title,
      author,
  }
}

QUERY VARIABLESには次の検索条件パラメータを指定
JSONのキーが$authorに入ります。

{
  "author": "Michael Crichton"
}

実行すると指定した条件の検索ができました
スクリーンショット 2018-06-10 19.01.59.png

フロントエンド(Apollo Client)

React + Apollo ClientからApollo Serverに通信を試みます。
まず、Apollo Clientをダウンロードします。

npm install --save apollo-boost react-apollo graphql-tag graphql

ApolloClientを生成して、ApolloProvider経由でアプリケーションコンポーネント<App />をwrapすることで<App />以下でgraphQLが使えるようになります。

index.jsx
/*globals module: false */
import React  from 'react'
import ReactDOM from 'react-dom'
import { hot } from 'react-hot-loader'
import { ApolloProvider } from 'react-apollo'
import ApolloClient from 'apollo-boost'
import App from './App'

const client = new ApolloClient({
  uri: 'http://localhost:5050/graphql',
})


const render = () => {
  ReactDOM.render(
    <ApolloProvider client={client}>
      <App />
    </ApolloProvider>
    ,
    document.getElementById('root'),
  )
}

// Webpack Hot Module Replacement API
hot(module)(render)

render()

App.jsです。qqlにクエリを記述します。
Queryコンポーネントで実際のAPIコールを行います。
API結果は{loading, error, data}は返却されます。
使いやすいようにgraphQLというHOCを作成してwrapしてみました。

App.jsx
import React from 'react'
import gql from 'graphql-tag'
import { Query } from 'react-apollo'

// GraphQLクエリ
const query = gql`
query ($author: String!){
    books(author: $author) {
        title,
      author,
  }
}
`

// HOC
const graphQL = (param) => (WrappedComponent) => (props) => (
  <Query
    query={query}
    variables={param}>
    {({loading, error, data}) => <WrappedComponent {...props} loading={loading} error={error} data={data} />}
  </Query>
)


class App extends React.Component {

  render () {
    const { loading, error, data } = this.props
    if (loading) return <p>Loading...</p>
    if (error) return <p>Error...</p>

    return (<div>
      {data.books.map(book =>
        <div key={book.title}>
          <h4>{book.title}</h4>
          <span>{book.author}</span>
        </div>
      )}
    </div>)
  }
}

export default graphQL({
  author: 'Michael Crichton',
})(App)

実行するとLoading表示からAPI取得後、データが表示されます。
スクリーンショット 2018-06-11 4.30.16.png

Reduxのreducerとreact-reduxのconnectの置き換えみたいなことができました。

参考:Apollo Client + React 入門

Apollo ClientでReduxの代用ができるのか

参考記事内でコラム: GraphQLはReduxを置き換えるのかとあったのですが、
Apollo ClientはGraphQLでの通信結果の状態管理はSSRなどを含めて通信周り(redux-thunkやredux-saga)の結果をReduxに入れてる箇所に関しては置き換えできそうでした。(主にreact-reduxのconnectとreducer周り)
逆に通信以外のアプリケーションデータに関して(例えばreact-router-reduxのような画面遷移状態をReduxに保持する等)を管理する機構が貧弱な(というか存在しない?)気がしました。(apollo-link-stateは通信周りだけっぽいですし・・・)
上記の事から、現状だとReduxを捨てることはできず、ローカルのStoreが2つ必要になるのでアンチパターンな気がしました。(逆にそこさえ解決できれば積極的に使いたい感)

Routingを見るとpropsにhistory渡せているっぽい?
GraphQLの設計思想自体はとても良いものなので通信周りはRest APIよりもスマートになると思いました。