The default way to work with Shopify API nowadays is Graphql. Most new API feature are strictly Graphql exclusive. This forces developers to switch no matter how we might like REST and dislike Graphql.
I began using Shopify Graphql API in my apps in 2021. At first, wrapping my head around the concept was challenging. After REST, Graphql felt like cryptic form of SQL over HTTP, full of nuances and lacking respect for HTTP standards. But I kept digging and eventually figured it out.
Problems
However, two issues kept bothering me:
- Excessive boilerplate code required to make a single Graphql call. In theory, Graphql calls look like simple HTTP POST requests. But in reality, for every request, you need to parse the response with nested objects, handle server errors, user errors, pagination, rate limits, and more.
- Lack of conventions and guidance on how to handle Graphql in Ruby. When you start using it in real-life, you have to figure out all the basics. It’s unclear how to handle errors, the best way to handle pagination, and even where to put Graphql queries and mutations within your app’s codebase.
Solution
To address these problems, I created shopify_graphql - a tiny wrapper on top of the official shopify_api gem. It adds additional layer of abstraction for Graphql calls, allowing them to be isolated and hiding the complexity from the rest of the app. Most importantly, it provides a set of conventions and best practices on how to structure and use Graphql in Rails apps. All of this is extracted from real-life experience building and maintaining multiple Shopify apps and has been battle-tested in production.
Getting started
To start using shopify_graphql, add it to Gemfile:
bundle add shopify_graphql
The gem relies on shopify_app for authentication, so no extra setup is required. However, you still need to wrap your Graphql calls with shop.with_shopify_session
:
shop.with_shopify_session do
# your calls to graphql
end
Core features
Conventions
- Organize all Graphql calls with wrapper classes. This will allow you to isolate Graphql complexity and hide it in one place from the rest of the app. Avoid using inline queries.
- Put all Graphql related code into
app/graphql
folder. This includes queries, mutations, and fields. - Use the following naming conventions to keep Graphql calls readable and organized:
- Use
Fields
suffix to name fields (e.g.AppSubscriptionFields
) - Use
Get
prefix to name queries (e.g.GetProducts
orGetAppSubscription
) - Use imperative verbs to name mutations (e.g.
CreateUsageSubscription
orBulkUpdateVariants
)
- Use
Here’s an example from real app:
As with any conventions, they might feel subjective, and you might feel the urge to change them. However, I strongly encourage you to trust and stick to them. This way, you’ll have one less choice to make and can focus your energy on something more useful.
Wrappers for queries
To create first query wrapper, you need to define new class and include ShopifyGraphql::Query
concern. This concern provides execute
method that will handle Graphql call and inital response parsing.
# app/graphql/get_product.rb
class GetProduct
include ShopifyGraphql::Query
QUERY = <<~GRAPHQL
query($id: ID!) {
product(id: $id) {
id
title
featuredImage {
source: url
}
}
}
GRAPHQL
def call(id:)
response = execute(QUERY, id: id)
response.data = parse_data(response.data.product)
response
end
private
def parse_data(data)
OpenStruct.new(
id: data.id,
title: data.title,
featured_image: data.featuredImage&.source
)
end
end
Under the hood, this will:
- Wrap Graphql call with tiny client
- Handle response errors. Instead of generic
ShopifyAPI::Errors::HttpResponseError
for all kind of errors you’ll get ActiveResource-like errors for different error codes (e.g.ShopifyGraphql::ResourceNotFound
orShopifyGraphql::TooManyRequests
) - Wrap network errors too, so instead of
JSON::ParserError
orErrno::ECONNRESET
you’ll get singleShopifyGraphql::ServerError
- Convert response to
OpenStruct
object so you can use dot notation on the returned data
In this example, you might see that we’re not only wrapping the Graphql call but also parsing the response from Shopify and turning it into a simple object that will feel more natural in Ruby.
To use this query class, you just need to call it and pass required parameters:
product = GetProduct.call(id: "gid://shopify/Product/12345").data
puts product.id
puts product.title
puts product.featured_image
Wrappers for mutations
Mutation wrappers are similar, with one small difference. In addition to regular API errors, mutation responses might include user errors. Since every Graphql mutation has unique response structure, the gem can’t parse these errors automatically. So, you need to call built-in helper handle_user_errors
on your parsed response.
# app/graphql/update_product.rb
class UpdateProduct
include ShopifyGraphql::Mutation
MUTATION = <<~GRAPHQL
mutation($input: ProductInput!) {
productUpdate(input: $input) {
product {
id
title
}
userErrors {
field
message
}
}
}
GRAPHQL
def call(input:)
response = execute(MUTATION, input: input)
response.data = response.data.productUpdate
handle_user_errors(response.data)
response
end
end
Usage is similar to queries. You just provide parameters or input hash:
response = UpdateProduct.call(input: { id: "gid://shopify/Product/123", title: "New title" })
puts response.data.product.title
Organizing fields
The gem doesn’t have any specific concerns for fields, but it suggests a convention on how to organize them instead. We need to extract the fields definition and parsing into separate class and call this class in our regular query wrappers.
# app/graphql/product_fields.rb
class ProductFields
FRAGMENT = <<~GRAPHQL
fragment ProductFields on Product {
id
title
featuredImage {
source: url
}
}
GRAPHQL
def self.parse(data)
OpenStruct.new(
id: data.id,
title: data.title,
featured_image: data.featuredImage&.source
)
end
end
# app/graphql/get_products.rb
class GetProducts
include ShopifyGraphql::Query
QUERY = <<~GRAPHQL
#{ProductFields::FRAGMENT}
query {
products(first: 5) {
edges {
cursor
node {
... ProductFields
}
}
}
}
GRAPHQL
def call
response = execute(QUERY)
response.data = parse_data(response.data.products.edges)
response
end
private
def parse_data(data)
return [] if data.blank?
data.compact.map do |edge|
OpenStruct.new(
cursor: edge.cursor,
node: ProductFields.parse(edge.node)
)
end
end
end
Built-in Graphql calls
The gem offers a few built-in Graphql calls. It’s not a long list and only covers subscriptions and private metafields at the moment. But I thinks it’s still useful to share them. They can be used as reference, and it’s just nice not having to copy-paste these very common calls from app to app. Here’s a list of existing built-in calls:
ShopifyGraphql::CancelSubscription
ShopifyGraphql::CreateRecurringSubscription
ShopifyGraphql::CreateUsageSubscription
ShopifyGraphql::GetAppSubscription
ShopifyGraphql::UpsertPrivateMetafield
ShopifyGraphql::DeletePrivateMetafield
The source code for them can be found here. I encourage you to share your own Graphql calls as well. Pick ones that are abstract enough from your app and could be potentially reused by others.
Rate limits
When working with Shopify API it’s important to know when it’s time to stop and take a break 😉. You would normally implement some sort of sleep/backoff logic in your app that will pause requests when Graphql API points are exhausted. To do so, you need throttle status information from Graphql responses. I made it easily accessible in shopify_graphql
gem:
response = GetProduct.call(id: "gid://shopify/Product/PRODUCT_GID")
response.points_left # => 1999
response.points_limit # => 2000.0
response.points_restore_rate # => 100.0
response.query_cost # => 1
response.points_maxed?(threshold: 100) # => false
More examples
You can find more real-world wrapper examples in gem repository. For example, how to work with collection queries, how to handle pagination, etc. https://github.com/kirillplatonov/shopify_graphql?tab=readme-ov-file#usage-examples
In addition to that, you can use built-in Graphql calls as an inspiration: https://github.com/kirillplatonov/shopify_graphql/tree/main/app/graphql/shopify_graphql
Conclusion
Using this gem has made working with Graphql much easier for me. I was able to address most of my problems with Graphql. At this point, maintaining existing Graphql calls and introducing new ones in apps feels pretty straightforward.
I believe that it is very beneficial to have a wrapper on top of the official library. It allows to hide a lot of complexity of Graphql and solves most of weak points of shopify_api
gem (like error handling and response parsing).
I really hope that official library will adopt some of ideas from shopify_graphql and we’ll see better out of the box experience for Graphql in the future. 🤞 But for now we have great alternative. 😎