Less painful way to work with Shopify Graphql API in Ruby. This library is a tiny wrapper on top of shopify_api
gem. It provides a simple API for Graphql calls, better error handling, and Graphql webhooks integration.
- Simple API for Graphql queries and mutations
- Conventions for organizing Graphql code
- ActiveResource-like error handling
- Graphql and user error handlers
- Auto-conversion of responses to OpenStruct
- Graphql webhooks integration for Rails
- Wrappers for Graphql rate limit extensions
- Built-in calls for common Graphql calls
shopify_api
v10+shopify_app
v19+
For
shopify_api
< v10 use0-4-stable
branch.
Add shopify_graphql
to your Gemfile:
bundle add shopify_graphql
This gem relies on shopify_app
for authentication so no extra setup is required. But you still need to wrap your Graphql calls with shop.with_shopify_session
:
shop.with_shopify_session do
# your calls to graphql
end
To better organize your Graphql code use the following conventions:
- Create wrappers for all of your queries and mutations to isolate them
- Put all Graphql-related code into
app/graphql
folder - Use
Fields
suffix to name fields (egAppSubscriptionFields
) - Use
Get
prefix to name queries (egGetProducts
orGetAppSubscription
) - Use imperative to name mutations (eg
CreateUsageSubscription
orBulkUpdateVariants
)
Click to expand
Definition:# app/graphql/get_product.rb
class GetProduct
include ShopifyGraphql::Query
QUERY = <<~GRAPHQL
query($id: ID!) {
product(id: $id) {
handle
title
description
}
}
GRAPHQL
def call(id:)
response = execute(QUERY, id: id)
response.data = response.data.product
response
end
end
Usage:
product = GetProduct.call(id: "gid://shopify/Product/12345").data
puts product.handle
puts product.title
Click to expand
Definition:# 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
Usage:
product = GetProduct.call(id: "gid://shopify/Product/12345").data
puts product.id
puts product.title
puts product.featured_image
Click to expand
Definition:# 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_product.rb
class GetProduct
include ShopifyGraphql::Query
QUERY = <<~GRAPHQL
#{ProductFields::FRAGMENT}
query($id: ID!) {
product(id: $id) {
... ProductFields
}
}
GRAPHQL
def call(id:)
response = execute(QUERY, id: id)
response.data = ProductFields.parse(response.data.product)
response
end
end
Usage:
product = GetProduct.call(id: "gid://shopify/Product/12345").data
puts product.id
puts product.title
puts product.featured_image
Click to expand
Definition:# app/graphql/get_products.rb
class GetProducts
include ShopifyGraphql::Query
QUERY = <<~GRAPHQL
query {
products(first: 5) {
edges {
node {
id
title
featuredImage {
source: url
}
}
}
}
}
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(
id: edge.node.id,
title: edge.node.title,
featured_image: edge.node.featuredImage&.source
)
end
end
end
Usage:
products = GetProducts.call.data
products.each do |product|
puts product.id
puts product.title
puts product.featured_image
end
Click to expand
Definition:# 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
Usage:
products = GetProducts.call.data
products.each do |edge|
puts edge.cursor
puts edge.node.id
puts edge.node.title
puts edge.node.featured_image
end
Click to expand
Definition:# 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
LIMIT = 5
QUERY = <<~GRAPHQL
#{ProductFields::FRAGMENT}
query($cursor: String) {
products(first: #{LIMIT}, after: $cursor) {
edges {
node {
... ProductFields
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
GRAPHQL
def call
response = execute(QUERY)
data = parse_data(response.data.products.edges)
while response.data.products.pageInfo.hasNextPage
response = execute(QUERY, cursor: response.data.products.pageInfo.endCursor)
data += parse_data(response.data.products.edges)
end
response.data = data
response
end
private
def parse_data(data)
return [] if data.blank?
data.compact.map do |edge|
ProductFields.parse(edge.node)
end
end
end
Usage:
products = GetProducts.call.data
products.each do |product|
puts product.id
puts product.title
puts product.featured_image
end
Click to expand
Definition:# 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
LIMIT = 5
QUERY = <<~GRAPHQL
#{ProductFields::FRAGMENT}
query($cursor: String) {
products(first: #{LIMIT}, after: $cursor) {
edges {
node {
... ProductFields
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
GRAPHQL
def call(&block)
response = execute(QUERY)
response.data.products.edges.each do |edge|
block.call ProductFields.parse(edge.node)
end
while response.data.products.pageInfo.hasNextPage
response = execute(QUERY, cursor: response.data.products.pageInfo.endCursor)
response.data.products.edges.each do |edge|
block.call ProductFields.parse(edge.node)
end
end
response
end
end
Usage:
GetProducts.call do |product|
puts product.id
puts product.title
puts product.featured_image
end
Click to expand
Definition:# app/graphql/get_collections_with_products.rb
class GetCollectionsWithProducts
include ShopifyGraphql::Query
COLLECTIONS_LIMIT = 1
PRODUCTS_LIMIT = 25
QUERY = <<~GRAPHQL
query ($cursor: String) {
collections(first: #{COLLECTIONS_LIMIT}, after: $cursor) {
edges {
node {
id
title
products(first: #{PRODUCTS_LIMIT}) {
edges {
node {
id
}
}
}
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
GRAPHQL
def call
response = execute(QUERY)
data = parse_data(response.data.collections.edges)
while response.data.collections.pageInfo.hasNextPage
response = execute(QUERY, cursor: response.data.collections.pageInfo.endCursor)
data += parse_data(response.data.collections.edges)
end
response.data = data
response
end
private
def parse_data(data)
return [] if data.blank?
data.compact.map do |edge|
OpenStruct.new(
id: edge.node.id,
title: edge.node.title,
products: edge.node.products.edges.map do |product_edge|
OpenStruct.new(id: product_edge.node.id)
end
)
end
end
end
Usage:
collections = GetCollectionsWithProducts.call.data
collections.each do |collection|
puts collection.id
puts collection.title
collection.products.each do |product|
puts product.id
end
end
Click to expand
Definition:
# 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:
response = UpdateProduct.call(input: { id: "gid://shopify/Product/123", title: "New title" })
puts response.data.product.title
Click to expand
PRODUCT_UPDATE_MUTATION = <<~GRAPHQL
mutation($input: ProductInput!) {
productUpdate(input: $input) {
product {
id
title
}
userErrors {
field
message
}
}
}
GRAPHQL
response = ShopifyGraphql.execute(
PRODUCT_UPDATE_MUTATION,
input: { id: "gid://shopify/Product/12345", title: "New title" }
)
response = response.data.productUpdate
ShopifyGraphql.handle_user_errors(response)
-
ShopifyGraphql::CurrentShop
:Equivalent to
ShopifyAPI::Shop.current
. Usage example:shop = ShopifyGraphql::CurrentShop.call puts shop.name
Or with locales (requires
read_locales
scope):shop = ShopifyGraphql::CurrentShop.call(with_locales: true) puts shop.primary_locale puts shop.shop_locales
-
ShopifyGraphql::CancelSubscription
-
ShopifyGraphql::CreateRecurringSubscription
-
ShopifyGraphql::CreateUsageSubscription
-
ShopifyGraphql::GetAppSubscription
-
ShopifyGraphql::UpsertPrivateMetafield
-
ShopifyGraphql::DeletePrivateMetafield
-
ShopifyGraphql::CreateBulkMutation
-
ShopifyGraphql::CreateBulkQuery
-
ShopifyGraphql::CreateStagedUploads
-
ShopifyGraphql::GetBulkOperation
Built-in wrappers are located in app/graphql/shopify_graphql
folder. You can use them directly in your apps or as an example to create your own wrappers.
The gem exposes Graphql rate limit extensions in response object:
points_left
points_limit
points_restore_rate
query_cost
And adds a helper to check if available points lower than threshold (useful for implementing API backoff):
points_maxed?(threshold: 100)
Usage example:
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
In custom apps, if you're using shopify_app
gem, then the setup is similar public apps. Except Shop
model which must include class method to make queries to your store:
# app/models/shop.rb
class Shop < ActiveRecord::Base
include ShopifyApp::ShopSessionStorageWithScopes
def self.system
new(
shopify_domain: "MYSHOPIFY_DOMAIN",
shopify_token: "API_ACCESS_TOKEN_FOR_CUSTOM_APP"
)
end
end
Using this method, you should be able to make API calls like this:
Shop.system.with_shopify_session do
GetOrder.call(id: order.shopify_gid)
end
If you're not using shopify_app
gem, then you need to setup ShopifyAPI::Context
manually:
# config/initializers/shopify_api.rb
ShopifyAPI::Context.setup(
api_key: "XXX",
api_secret_key: "XXXX",
scope: "read_orders,read_products",
is_embedded: false,
api_version: "2024-07",
is_private: true,
)
And create another method in Shop model to make queries to your store:
# app/models/shop.rb
def Shop
def self.with_shopify_session(&block)
ShopifyAPI::Auth::Session.temp(
shop: "MYSHOPIFY_DOMAIN",
access_token: "API_ACCESS_TOKEN_FOR_CUSTOM_APP",
&block
)
end
end
Using this method, you should be able to make API calls like this:
Shop.with_shopify_session do
GetOrder.call(id: order.shopify_gid)
end
Warning
ShopifyGraphql webhooks are deprecated and will be removed in v3.0. Please use shopify_app
gem for handling webhooks. See shopify_app
documentation for more details.
The gem has built-in support for Graphql webhooks (similar to shopify_app
). To enable it add the following config to config/initializers/shopify_app.rb
:
ShopifyGraphql.configure do |config|
# Webhooks
webhooks_prefix = "https://#{Rails.configuration.app_host}/graphql_webhooks"
config.webhook_jobs_namespace = 'shopify/webhooks'
config.webhook_enabled_environments = ['development', 'staging', 'production']
config.webhooks = [
{ topic: 'SHOP_UPDATE', address: "#{webhooks_prefix}/shop_update" },
{ topic: 'APP_SUBSCRIPTIONS_UPDATE', address: "#{webhooks_prefix}/app_subscriptions_update" },
{ topic: 'APP_UNINSTALLED', address: "#{webhooks_prefix}/app_uninstalled" },
]
end
And add the following routes to config/routes.rb
:
mount ShopifyGraphql::Engine, at: '/'
To register defined webhooks you need to call ShopifyGraphql::UpdateWebhooksJob
. You can call it manually or use AfterAuthenticateJob
from shopify_app
:
# config/initializers/shopify_app.rb
ShopifyApp.configure do |config|
# ...
config.after_authenticate_job = {job: "AfterAuthenticateJob", inline: true}
end
# app/jobs/after_install_job.rb
class AfterInstallJob < ApplicationJob
def perform(shop)
# ...
update_webhooks(shop)
end
def update_webhooks(shop)
ShopifyGraphql::UpdateWebhooksJob.perform_later(
shop_domain: shop.shopify_domain,
shop_token: shop.shopify_token
)
end
end
To handle webhooks create jobs in app/jobs/webhooks
folder. The gem will automatically call them when new webhooks are received. The job name should match the webhook topic name. For example, to handle APP_UNINSTALLED
webhook create app/jobs/webhooks/app_uninstalled_job.rb
:
class Webhooks::AppUninstalledJob < ApplicationJob
queue_as :default
def perform(shop_domain:, webhook:)
shop = Shop.find_by!(shopify_domain: shop_domain)
# handle shop uninstall
end
end
The gem is available as open source under the terms of the MIT License.