We first started using Shopify’s GraphQL API back in 2018 to run faster inventory edits with our Ablestar Bulk Product Editor. It was our first experience with GraphQL and, needless to say, our code was a bit rough.

Eventually, we got everything working, but we were manually building our GraphQL queries and missing out on key GraphQL features like schema validation. As we added more GraphQL API calls to our apps and upgraded between Shopify API versions, it became increasingly difficult to keep track of different queries and ensuring they all ran correctly.

We’ve come a long way in the last seven years, and I wanted to share our journey toward our current approach, which includes autocomplete and schema validation for our GraphQL apps. This setup makes it much easier to upgrade between API versions. It’s not perfect, but I hope sharing our progress can help others as they integrate with Shopify’s GraphQL API.

The example code below is in Python, but the general principles apply to most languages.

1. Manually Build GraphQL Queries

When we started with GraphQL, we built queries by simply joining strings together. For example, if we wanted to retrieve product details, the code looked like this:

product_id = "1234567890"

query_str = """
{
  product(id: "gid://shopify/Product/""" + product_id + """") {
    id
    title
    handle
    descriptionHtml
  }
}
"""

client.execute(query=query_str)

Here, the product ID is directly embedded into query_str, which is passed to the GraphQL API. This approach let us get started quickly, but it was easy to create invalid queries by forgetting to escape certain characters. It also didn’t work well with code formatters, making long-term maintenance harder.

Pros:

  • Quick to get started

Cons:

  • Code can get messy with multiple variables
  • Manually escaping characters like spaces and quotes gets tricky
  • Hard to keep track of all your different GraphQL queries
  • Autoformatting tools don’t work

2. Separate GraphQL Query and Variables

Our next step (as in, over a year later when we were running into the limitations of method #1) was to split out the variables and pass them separately to the GraphQL API. Instead of including parameters directly in the query, we declare variables in the query and then pass them separately to the GraphQL API.

For example, retrieving product details now looks like this:

query_str = """
query productGetDetails($productId: ID!){
  product(id: $productId) {
    id
    title
    handle
    descriptionHtml
  }
}
"""

variables_dict = {
  "productId": "gid://shopify/Product/1234567890"
}

client.execute(query=query_str, variables_dict=variables_dict)

The key line is query productGetDetails($productId: ID!) which has the following:

  • query - Used when declaring a query (or mutation) with variables.
  • productGetDetails - An optional name for the query, helpful for logging.
  • $productId: ID! - Declares a variable named $productId of type ID, marked as required (!).

We can then use the $productID variable further down in the query like product(id: $productId).

📘 Query Names

Following Shopify’s convention of [object][action] (e.g., orderDelete, productGet) can be helpful. If you split queries into separate files, they’ll be neatly grouped by file name (more on that later).

Finding the correct type for the variables might mean an extra trip to the GraphQL documentation, but it’s usually straightforward since types are clearly listed alongside each argument (usually they’re one of ID, String, Int or Boolean).

Pros:

  • Variables are properly escaped
  • It’s easier to see what the query does at a glance, and the query name is useful for logging.
  • Some tools can now provide autocomplete and linting for the GraphQL code blocks.

Cons:

  • Declaring variables with the correct type requires a bit more work up front.
  • It’s still difficult to keep track of all your queries.

3. Move GraphQL Queries to Separate Files

This brings us to our current approach. We now store each query in a standalone .graphql file and updated our client library to load that file when the query is called. Retrieving product details looks like this::

query productGetDetails($productId: ID!){
  product(id: $productId) {
    id
    title
    handle
    descriptionHtml
  }
}

And we call it like this:

variables_dict = {
  "productId": "gid://shopify/Product/1234567890"
}

client.run_query("productGetDetails", variables_dict=variables_dict)

This method has more initial overhead than the previous two, but it’s much easier to maintain, especially when moving between API versions. More specifically:

  • Code formatting and autocomplete work well in VSCode with the GraphQL: Language Feature Support extension
  • All GraphQL queries live in a single directory. Since query file names start with the object name (e.g., product, order), they remain grouped logically.
  • The GraphQL: Language Feature Support VSCode extension highlights deprecated fields and marks GraphQL files in yellow, making it easy to see needed updates.

I won’t get into the details of the run_query() function here, but two additional features it supports are fragments and caching. Fragments let you request the same set of fields across different queries, and run_query() will automatically includes them from a separate fragments/ directory. All of this parsing and fragment lookup is also cached, so we don’t need to scan the file system every time we make a query.

Pros:

  • Autocomplete, formatting, and deprecation warnings work great in .graphql files.
  • Easy to keep track of related queries.
  • Faster upgrades between API versions because deprecated or removed fields are clearly flagged.

Cons:

  • You’ll need to create an equivalent of run_query() that can load the queries from the filesystem.
  • Copying all your queries to .graphql files is tedious, but it’s a one-time effort.

Final Thoughts

We’re currently using the third method - one GraphQL query per file - for Ablestar Bulk Product Editor and I’m happy with the results. It works at our scale (editing millions of products per day) and makes upgrading to newer Shopify API versions more straightforward.

For instance, when we moved to Shopify API version 2024-07, any removed fields (like fulfillmentService) were flagged in VSCode, and we could quickly locate and update them. Having all the queries in a single place, properly named and formatted, also just feels good.

There are still other areas we’d like to explore, including codegen and adding schema validation to our build process, but those are topics for another day.

If you have any feedback or other approaches, I’d love to hear about them on Twitter.