Shopifyに学ぶ!GraphQL API設計5つのポイント
この記事の所要時間:12分
Shopifyは、ノーコードのオンラインショップ構築サービスを提供しています。
また、Shopifyは外部開発者向けのAPIも提供しており、”アプリ”と呼ばれる拡張機能を開発できます。
shopify app storeには、Shopify APIを使った多様なアプリが一般に公開されています。
Shopifyが対象とする事業領域は複雑ですが、洗練されたAPIや、それを活用した多様なアプリによって、柔軟なニーズに応えています。
この記事では、Shopify GraphQL APIの設計から、5つの設計プラクティスを導きます。
各章の最後に、設計プラクティスが1つずつ書かれています。
コネクションとページネーション
グラフ理論では、オブジェクトをNode(ノード)といい、オブジェクト同士のつながりをEdge(エッジ)といいます。
GraphQLでは、NodeとEdgeを用いたグラフ構造を実装する手段として、コネクションという仕組みを提供しています。
Shopify GraphQL APIでも、主要なオブジェクトにはコネクションが実装されています。
Shopify GraphQL APIの主要なConnectionを紹介します。
コネクション名 | 型名 | 説明 |
---|---|---|
customers | CustomerConnection | 顧客のリスト |
deliveryProfiles | DeliveryProfileConnection | 配達のリスト |
inventoryItems | InventoryItemConnection | 在庫のリスト |
orders | OrderConnection | 商品注文のリスト |
products | ProductConnection | 商品のリスト |
subscriptionContracts | SubscriptionContractConnection | 定期購入契約のリスト |
試しに、productsにクエリーを実行してみます。
query {
products(first: 3) {
edges {
node {
title
}
}
}
}
{
"data": {
"products": {
"edges": [
{
"node": {
"title": "wonderful T-shirt"
}
},
{
"node": {
"title": "white T-shirt"
}
},
{
"node": {
"title": "monocrotonic T-shirt"
}
}
]
}
},
"extensions": {
"cost": {
"requestedQueryCost": 5,
"actualQueryCost": 5,
"throttleStatus": {
"maximumAvailable": 1000.0,
"currentlyAvailable": 995,
"restoreRate": 50.0
}
}
}
}
グラフ構造に対して検索し、グラフ構造の一部が返ってきています。
たとえショップに膨大な数の商品があったとしても、全ての商品データを一気に取得するような場面は少ないでしょう。
そもそも、膨大なデータを一気に取得するのは時間がかかるので現実的ではありません。
先ほどのように、first引数を指定して、いくつかのオブジェクトだけ取得したりします。
GraphQLでは、コネクションから得られる膨大なデータを少しずつ取得するために、ページネーションという仕組みが提供されています。
first引数もページネーションを実現する1つの仕組みです。
ページネーションでは、コネクションに引数を渡すことで、任意の位置から、任意の数だけ、Nodeを取得できます。
edgesには、cursorというフィールドがあります。
cursorを使うことで、任意の位置からNodeを取得できます。
先ほど取得した最後のNodeの直後から、3つのNodeを取得する例です。
query {
productVariants(first: 3, after: "eyJsYXN0X2lkIjozOTkyOTg3NDM4MzAxMiwibGFzdF92YWx1ZSI6IjM5OTI5ODc0MzgzMDEyIn0=") {
edges {
node {
displayName
}
cursor
}
}
}
{
"data": {
"productVariants": {
"edges": [
{
"node": {
"displayName": "white T-shirt - Default Title"
},
"cursor": "eyJsYXN0X2lkIjozOTkyOTkxMTkzNTE0MCwibGFzdF92YWx1ZSI6IjM5OTI5OTExOTM1MTQwIn0="
},
{
"node": {
"displayName": "monocrotonic T-shirt - Red"
},
"cursor": "eyJsYXN0X2lkIjozOTk4Mjc2MzYwNjE4MCwibGFzdF92YWx1ZSI6IjM5OTgyNzYzNjA2MTgwIn0="
},
{
"node": {
"displayName": "monocrotonic T-shirt - Blue"
},
"cursor": "eyJsYXN0X2lkIjozOTk4Mjc2MzYzODk0OCwibGFzdF92YWx1ZSI6IjM5OTgyNzYzNjM4OTQ4In0="
}
]
}
},
# 省略
}
firstで個数を指定し、afterとcursorで位置を指定しました。
Shopify GraphQL APIのコネクションでよく使われる引数がこちらです。
引数名 | 型 | 説明 |
after | String | cursorで位置を指定し、その直後から要素を取得する。 |
before | String | cursorで位置を指定し、その直前から要素を取得する。 |
first | Int | 取得する要素の数。afterが指定されていれば、その位置の直後から取得する。指定された数の要素がなければ、あるだけの要素を返す。 |
last | Int | 取得する要素の数。beforeで指定されていれば、その位置の直前から取得する。指定された数の要素がなければ、あるだけの要素を返す。 |
reverse | Boolean | 全ての操作の前に要素の順番を逆にする。 |
イメージ図がこちらです。
Shopify GraphQL APIは、コネクションに関するメタデータも提供しています。
それが、pageInfoです。
pageInfoには以下の2つのフィールドがあります。
- hasNextPage(Boolean)・・・取得した最後のNodeの後にもNodeが存在するかどうか
- hasPreviousPage(Boolean)・・・取得した最初のNodeの前にもNodeが存在するかどうか
クエリーの例がこちらです。
query {
productVariants(first: 3) {
edges {
node {
displayName
}
}
pageInfo {
hasNextPage
hasPreviousPage
}
}
}
{
"data": {
"productVariants": {
# 省略,
"pageInfo": {
"hasNextPage": true,
"hasPreviousPage": false
}
}
},
# 省略
}
最初の3つのNodeを取得したので、hasPreviousPageがfalseとなっています。
設計のポイント
複数のオブジェクトを取得する手段としては、リストも使われますが、引数は渡せませんので、多数のオブジェクトをうまくコントロールできません。
Shopify GraphQL APIでは、CustomerやOrderなど、数が多くなりうるオブジェクトには、コネクションが実装されています。
GraphQL APIの設計においては、オブジェクトの数が一度に取得できないほど多くなりうるのであれば、コネクションの実装を検討するのが良いでしょう。
クエリーレベルとロジックレベルのエラー
GraphQLはスキーマにそぐわないクエリーに対して、errorsフィールドをトップレベルに含むjsonを返します。
Shopify GraphQL APIの場合は、このようなレスポンスになります。
{
"errors": [
{
"message": "Field 'name' doesn't exist on type 'ProductVariant'",
"locations": [
{
"line": 19,
"column": 5
}
],
"path": [
"query",
"productVariant",
"name"
],
"extensions": {
"code": "undefinedField",
"typeName": "ProductVariant",
"fieldName": "name"
}
}
]
}
一方、ロジックレベルのエラーはUserError型で返されます。
UserError型は、以下の2つのフィールドを持っています。
field | [String!] | エラーを引き起こしたフィールドのリスト |
message | String! | エラーメッセージ |
mutationが成功した場合は、userErrorsは長さ0のリストとして返ってきます。
{
"data": {
"customerDelete": {
"deletedCustomerId": "gid:\/\/shopify\/Customer\/5341881008292",
"shop": {
"name": "アプリ開発のてすと"
},
"userErrors": []
}
},
# 省略
}
実行に失敗すると、エラー元のフィールドのリストとエラーメッセージが返ってきます。
{
"data": {
"customerDelete": {
"deletedCustomerId": null,
"shop": {
"name": "アプリ開発のてすと"
},
"userErrors": [
{
"field": [
"id"
],
"message": "Customer can't be found"
}
]
}
},
# 省略
}
設計のポイント
Shopify GraphQL APIでは、レスポンスのトップレベルで返されるerrorsはクエリーレベルのエラーのみを返します。
ロジックレベルのエラーは、userErrors型で返すようになっています。
クエリーレベルのエラーとロジックレベルのエラーが分かれていることで、それぞれのエラーに適したフォーマットでレスポンスを返せます。
クライアントにとっては、エラーハンドリングが楽になります。
スカラー型
GraphQLクエリーは、木構造ですが、一番下には常にスカラー型が存在します。
GraphQLでは、IntやStringなど、全ての基本型は、スカラー型として定義されています。
開発者は独自のスカラー型を定義することができます。
独自のスカラー型を定義することで、その型のシリアライズやバリデーションも独自に実装することができます。
Shopify GraphQL APIには、Dateや、URL、HTMLといったスカラー型が独自に定義されています。
設計のポイント
スカラー型を定義すると、GraphQLレイヤーで値のフォーマットに関するバリデーションが行われます。
そのため、ビジネスロジックレイヤーは、ビジネスドメインに集中できます。
スカラー型を定義することのデメリットもあります。
最も大きなデメリットは、クライアントの処理が複雑になることでしょう。
スカラー型を定義すると、クライアントは、errorsフィールドで返るエラーと、userErrors型で返るエラーの、両方に対応する必要があります。
このことから、Shopify GraphQL APIでは、メールアドレスの入力はEmail型ではなくString型となっています。
スカラー型を定義するかどうかはケースバイケースです。
場合に応じて検討しましょう。
Shopifyが公開しているチュートリアルでは、次のように書かれています。
フォーマットが明確であり、クライアント側の検証が複雑な場合には、入力に対して弱い型付け(EmailではなくString)を行うこと。これによりサーバーは一度にすべての検証を実行し、単一のフォーマットでエラーを報告することになり、結果としてクライアントが非常にシンプルになる。
https://github.com/Shopify/graphql-design-tutorial/blob/master/lang/TUTORIAL_JAPANESE.md
Mutationの粒度
RESTful APIでは、Create、Read、Update、Deleteという4つの操作を使います。
一方、GraphQLでは、ReadがQueryによって提供され、残りは全てMutationで提供されます。
Shopify GraphQL APIのCustomerオブジェクトに対して実行できるMutationがこちらです。
customerCreate | 顧客の作成 |
customerDelete | 顧客の削除 |
customerUpdate | 顧客の基本情報の更新 |
customerAddTaxExemptions | 顧客に免税ルールを追加する |
customerRemoveTaxExemptions | 顧客に免税ルールを削除する |
customerReplaceTaxExemptions | 顧客の免税ルールを入れ替える |
customerGenerateAccountActivationUrl | 顧客アカウント有効化のためのURLを発行する |
customerUpdateDefaultAddress | 顧客の住所を更新する |
設計のポイント
大抵の場合、CreateとDeleteはそれぞれ1つのMutationで事足りますが、Updateを1つのMutationで行おうとするとサーバ側の負担が重くなりがちです。
UpdateMutationに全ての更新系操作が集約していると、更新される値を判別する処理がなくてはなりません。
また、ビジネスドメイン上のルールを保証するためのバリデーションは複雑になるでしょう。
GraphQLでは、RESTful APIにおけるUpdateをより細かい粒度に分割するのが良いプラクティスです。
Shopifyでは、クライアント側のユースケースごとにMutationが用意されています。
メタフィールド
Shopify GraphQL APIには、APIユーザがオブジェクトに対して自由にフィールドを追加できる、メタフィールドという仕組みがあります。
メタフィールドを追加するためには、オブジェクトをUpdateするMutationを実行します。
例として、Productに商品の素材を表すフィールドを追加してみます。
mutation {
productUpdate(input: {
id: "gid:\/\/shopify\/Product\/6704170827940",
metafields: [
{
namespace: "property",
key: "material",
value: "silk"
valueType: STRING,
}
],
}) {
product {
metafields(first: 10) {
edges {
node {
namespace
key
value
}
}
}
}
}
}
{
"data": {
"productUpdate": {
"product": {
"metafields": {
"edges":
{
"node": {
"namespace": "property",
"key": "material",
"value": "silk"
}
}
]
}
}
}
},
# 省略
}
メタフィールドを作成する際の入力値が以下になります。
id | ID | メタフィールドの識別子 |
---|---|---|
valueType | String | フィールドの型 |
namespace | String | フィールドの名前空間 |
description | String | フィールドの説明 |
key | String | フィールド名 |
value | String | フィールドの値 |
設計のポイント
特定の利用用途を満たすために開発されたShopifyアプリの中には、メタフィールドを活用するものが多く存在します。
Shopifyが、あらゆる用途で利用可能となっている背景には、メタフィールドの活用があります。
また、メタフィールドによって特殊な利用用途を満たせるので、APIをシンプルに保てます。
多様なクライアントが想定されるGraphQL APIを設計する際には、メタフィールドの導入を検討すると良いでしょう。
まとめ
GraphQLで、ある型の、複数のオブジェクトを取得する手段には、リストとコネクションがあります。
リストは比較的少数のオブジェクトを取得するのに向いています。
コネクションは多数のオブジェクトを分割して取得することができます。
GraphQLでは、レスポンスのトップレベルでクエリーレベルのエラーが返されます。
一方、ロジックレベルのエラーを返す手段は特には提供されていませんが、
Shopify GraphQL APIではuserErrors型で返します。
クエリーレベルのエラーとロジックレベルのエラーが返ってくる場所が分かれていることで、クライアントの処理がシンプルになります。
スカラー型を定義すると、ビジネスロジックレイヤーはビジネスドメインに集中できます。
しかし、クライアントのエラーハンドリングが多少複雑になります。
そのため、スカラー型の定義は、ケースバイケースで考えると良いでしょう。
Updateを1つのMutationで行おうとするとサーバ側の負担が重くなりがちです。
そのため、GraphQLでは、RESTful APIにおけるUpdateをより細かい粒度に分割するのが良いプラクティスです。
多様なクライアントが想定されるGraphQL APIを設計する際には、メタフィールドの導入が有力な選択肢となります。
是非、今回紹介した設計プラクティスを参考にして、GraphQL APIを設計してみてください。
【最後に】Shopify GraphQL API徹底解説シリーズ
ショピナビでは、これからShopify開発を始める方のために、GraphQL APIについてわかりやすく解説した記事を公開しています。ぜひ以下の記事も合わせてご覧ください。