Edited at

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

More than 1 year has passed since last update.


はじめに

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

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,
}
}


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"
}

実行すると指定した条件の検索ができました


フロントエンド(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取得後、データが表示されます。

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よりもスマートになると思いました。