
express-graphql + vue-apollo で GraphQLを試してみた
こんにちは。エンジニアの@elcih17です。
この記事を書いた頃はインターンでしたが、現在は新卒エンジニアとして日々奮闘しています。
最近、APIの設計に関することを調べていたら「GraphQL」という言葉をよく見かけました。
API用の技術として注目されているそうで、REST APIの次のパラダイムだという意見もあるようです。
どんな技術なのかとても気になりHOW TO GRAPHQLのチュートリアルと簡単な実装をしてみたら、なんとなくGraphQLのことがわかったので書いてみます。
実装例
- バックエンド (express-graphql) graphql-server
- フロントエンド (vue-apollo) vue-apollo-example
GraphQL とは
- API用のクエリ言語
特徴
- シングルエンドポイント
- 複数のリソースの欲しいデータを1リクエストで取得できる
- スキーマ定義と型システム
機能
Query
RESTにおけるGET
idが"1"のユーザーの名前と年齢を取得するGraphQLは次のように書けます。
query {
user(id:"1") {
name
age
}
}
response
{
"data": {
"user": {
"name": "elcih17",
"age": 22
}
}
}
Mutation
RESTにおけるPOST、PUT、DELETE
新規ユーザーを作成するGraphQLは次のように書けます。
mutation {
createUser(id: "2", name: "KARTE", age:2) {
name
age
}
}
response
{
"data": {
"user": {
"id": "2",
"name": "KARTE",
"age": 2
}
}
}
Subscription
QueryとMutationの実行をsubscribeできる
試してないので詳しくはわかりませんが、データの更新を受け取って何かしたいときに使えそうです。
GraphQLの何が良いのか
- 不要なデータの取得と無駄なリクエストを防げる
- スキーマと型定義によってAPIが提供する機能が明確になる
- GraphiQLが便利
不要なデータの取得と無駄なリクエストを防げる
複数のリソースの欲しいデータを1リクエストで取得できる ので無駄が無くなります。
例えば、あるユーザーの名前と投稿記事タイトルと記事についたコメントを取得するケースを考えてみます。
RESTの場合
GET /users/:id
GET /users/:id/posts
GET /posts/:id/comments
- 取得したデータからそれぞれ名前、記事のタイトル、コメントを抽出します
GraphQLの場合
/graphql
に対し下記のようなクエリをリクエストします
必要なデータのみ取得しているのでデータを抽出する必要はありません
query {
user(id: "1") {
name
posts {
title
comments {
content
}
}
}
}
スキーマと型定義によってAPIが提供する機能が明確になる
- 入出力と型を定義することによって何が出来るかかが分かりやすい
- 後述するGraphiQLで見ることが出来るAPIドキュメントはスキーマ定義から自動生成される
GraphiQLが便利
- GraphQLのIDE
- APIドキュメントの閲覧とクエリの実行ができる
- デバッグがしやすい
- クエリをサクサク試せる
- エラーメッセージがわかりやすい
GraphQLサーバーの例(express-graphql)
Query、Mutationの機能紹介で例示したクエリを実行可能にするための実装はこのようになります
GraphQLスキーマの定義 (GraphQLの入出力と型定義)
!
はrequired
type Query {
user(id: String!): User! // relation
}
type Mutation {
createUser(id: String!, name: String!, age: Int!): User // relation
}
type User {
id: String!
name: String!
age: Int!
}
リゾルバ関数の実装 (GraphQLとバックエンド処理のマッピング)
const users = [
{
id: '1',
name: 'elcih17',
age: 22
}
]
const resolvers = {
Query: {
user: (root, {id}) => {
return users.find(user => user.id === id)
}
},
Mutation: {
createUser: (root, args) => {
const {id, name, age} = args
const newUser = {id, name, age}
users.push(newUser)
return newUser
}
}
};
GraphQLオブジェクトとrelation(関連づけ)を使う
標準のオブジェクトタイプ(Query
、Mutation
、Subscription
)以外にも独自のオブジェクトを定義できます。
また、定義したオブジェクト同士のrelationが可能です。
スキーマ
type User {
name: String!
posts: [Post]
}
type Post {
author: User!
title: String!
}
relationを使えば次のようなクエリを受け取った際にデータを構築するのが簡単になります。
query {
user(id: "1") {
posts {
title
}
}
}
リゾルバ (データの構築)
Query: {
user: (root, args) => {
const {id} = args
return users.find(user => user.id === id)
}
},
User: {
posts: (author) => {
const {id} = author
return posts.filter(post => post.author_id === id)
}
}
GraphQLクエリの実行の流れについてはこの記事が分かりやすいです。
実装例ではMongoDBに接続して実装してみました。
所感
- GraphiQLのおかげでデバッグがしやすくて良い
- GraphQLのスキーマ定義とDBのスキーマ定義が二重管理になって面倒
- DBのスキーマからGraphQLスキーマを生成するようなライブラリが出てくれば楽になるかも
フロントエンドGraphQL
現在、GraphQLを扱うためのClientに Apollo と Relay があります。
RelayはReact用に構築されていてApolloはそのほかのフレームワークにも対応しているようです。
React + Apolloという組み合わせもあるので、Reactを使っている人はRelayとApolloを比べてみても面白いかもしれません。
今回はvue-apolloを使うためにApolloを使用してみました。
一部実装例を載せておきます。
src/main.js
import 'isomorphic-fetch'
import Vue from 'vue'
import {ApolloClient, createNetworkInterface} from 'apollo-client'
import VueApollo from 'vue-apollo'
import App from './App.vue'
const uri = 'http://localhost:3000/graphql'
const networkInterface = createNetworkInterface({
uri,
transportBatching: true,
})
Vue.use(VueApollo)
const apolloClient = new ApolloClient({networkInterface})
const apolloProvider = new VueApollo({
defaultClient: apolloClient,
})
new Vue({
el: '#app',
apolloProvider,
render: h => h(App),
})
src/App.vue
<!-- template -->
<div id="app">
<h1> Vue Apollo Example </h1>
<p>{{hello}}</p>
<label>name</label>
<input type="text" v-model="userName">
<label>age</label>
<input type="number" v-model="userAge">
<a href="#" @click="createUser">createUser</a>
<div v-if="createdUser">
<p>User created!</p>
<p>name: {{createdUser.name}}</p>
<p>age: {{createdUser.age}}</p>
</div>
</div>
// script
import gql from 'graphql-tag';
const helloGQL = gql`
query {
hello
}
`;
const createUserGQL = gql`
mutation ($userName: String!, $userAge: Int!){
createUser(name: $userName, age: $userAge) {
_id
name
age
}
}
`
export default {
name: 'app',
data() {
return {
hello: '',
userName: '',
userAge: '',
createdUser: null
};
},
apollo: {
hello: {
query: helloGQL,
}
},
methods: {
createUser() {
this.$apollo.mutate({
mutation: createUserGQL,
variables: {
userName: this.userName,
userAge: this.userAge
}
}).then(res => {
this.createdUser = res.data.createUser
}).catch(err => {
console.error(err);
})
}
}
};
詳細は実装例をご覧ください。
所感
- 欲しいデータだけを1リクエストで取得できるのは確かに良さそう
- しかし、rootのデータを取得してロジックで整形みたいなことをするとRESTとあまり変わらない
- 不要なデータ取得と無駄なリクエストを実際に減らせるかは使用者次第という感じ
- エンドポイント × GraphQLクエリの分だけ管理が必要なので複雑になってくるとツラそう
プロダクションで使えるか?
現在プロダクション環境でGraphQL APIを運用しているサービスはGithub、Coursera、Twitterなどがあるようです。
GithubのGraphQL API v4を見る限り、運用の仕組みを作れば十分使えそうな気がします。
プロダクションで運用・提供するに当たって考慮する必要がありそうなこと
- バックエンド
- クエリ(スキーマ)のライフサイクル管理
- deprecatedフィールドなど
- クエリのキャッシュ管理
- セキュリティ
- 巨大なクエリ、複雑なクエリへの対策
- タイムアウト処理
- クエリの実行回数制限
- 1クエリのfield数制限
- 1クエリのネスト(深さ)制限
- GithubのRate limitsが参考になります
- 巨大なクエリ、複雑なクエリへの対策
- クエリ(スキーマ)のライフサイクル管理
- フロントエンド
- クエリが自由にかけて属人性が高い
- 規約が必要になることもあるかも?
まとめ
- クエリが直感的に書ける
- 必要なデータだけをリクエストできるので分かりやすい
- 自由に書けるので複雑なクエリをリクエストされるリスクもある
- GraphiQLが便利
- 不要なデータ取得、無駄なリクエストを減らせるのは大きなメリット
- プロダクションで運用する際にはセキュリティ対策は必須
以上で説明を終わります。
GraphQLについて少しはわかっていただけたでしょうか?
今回紹介できなかった機能を使えばもっと色々とできて面白そうなので興味のある方はぜひ試してみてください。
最後に
ウェブ接客プラットフォーム「KARTE」を運営するプレイドでは、 KARTEを支える技術に興味を持つエンジニア(インターンも!)を募集しています。
詳しくはプレイドの採用ページか、Wantedlyをご覧ください。
記事をシェア