LoginSignup
134
130

More than 5 years have passed since last update.

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

Last updated at Posted at 2018-06-10

はじめに

今回のサンプルは以下に用意しました。
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よりもスマートになると思いました。

134
130
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
134
130