Kirill Platonov
Kirill Platonov
All Posts May 12, 2024

Less painful way to work with Shopify Graphql API in Ruby

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

  1. 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.
  2. Put all Graphql related code into app/graphql folder. This includes queries, mutations, and fields.
  3. 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 or GetAppSubscription)
    • Use imperative verbs to name mutations (e.g. CreateUsageSubscription or BulkUpdateVariants)

Here’s an example from real app: Example shopify_graphql usage in 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 or ShopifyGraphql::TooManyRequests)
  • Wrap network errors too, so instead of JSON::ParserError or Errno::ECONNRESET you’ll get single ShopifyGraphql::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. 😎

Subscribe to get new blog posts via email

Or grab the RSS feed