GraphQL API Design - Major Issues

We are gradually migrating our REST integration to GraphQL. In each part of the migration we have found ourselves pulling our hair out due to the ‘design’ of the GraphQL API, or rather, the complete lack of it.

This is a continuation of the below discussion.

GraphQL Api is a Hot Mess

As some background we have a middleware/integration platform for medium size businesses. We have integrated with Shopify from the start. We offer about 30 functional (read/write) endpoints with Shopify, covering order processing, inventory/item/pricing and customer handling.

I can understand that as Shopify grows, there probably needs to be some difficult decisions, however….

Problem

The GQL API seems to be a direct view of the Shopify’s database.

The extreme fine grained catalog/publication mutations is an example. Why do we need to issue a separate GraphQL mutation to link the publication to the catalog, when there is likely a 1:1 relationship?

Other examples include the companyLocationAssignTaxExemptions, LocationCreateTaxRegistration and see below.

The general sense of the GQL API seems to have no regard to sensible defaults that I assume most of your customers would only ever need.

Problem

What took a single REST call now takes multiple GqL invocations.

For example, creating products with variants now takes at minimum 2 GQL calls - one for the product, one for the primary variant. A sensible implementation would be to fold these operations into one.

If you wish to create/update a product with variants you now three GQL invocations? Why do we need a productCreate vs. productUpdate, and a productVariantsBulkCreate vs. productVariantsBulkUpdate mutations? This requires splitting out the variants into create and update sets and submitting separate requests? The distinguishing factor is variant id → the Shopify API should just use that and perform the necessary insert/update (upsert) handling.

Similarly the companyCreate is a good API, but an update now requires 5 separate mutations!

Problem

No Atomicity

Because of the two problems above, transactions that were (we assumed) atomic in the REST API are now spliced over multiple separate invocations. An error can result in logical writes which are half written. Salesforce, for example, allows you to combine operations into a single request which will fail atomically.

Problem

No External Ids

We find ourselves having to query Shopify to translate a description (or partial description) into an id so that we can make the insert/update. A better approach (also see Salesforce) would be to allow most records/objects to be tagged with an external id. This would (in our opinion man) reduce the number of queries and permit objects to be identified with the external id.

The Upshot

What was a nice solution to integrate with, is now tedious. For each of our write integration points we now need to create a public façade where the API resembles the well-designed, nice REST API, but our new backend implementation now does all the fiddly/fine-grained GQL queries and mutations. You may not care, but our small company has gone from being a Shopify advocate to one where we now sit on the fence.

1 Like

Hey @James_Hutchison, thanks for the detailed write-up.

Typically, the fine-grained nature of GraphQL that you’re seeing is considered a benefit over REST in many ways, but I do see in situations where you’re working with a large set of data on a single object, that REST does make it simpler.

Some of your main concerns have actually been addressed in recent releases. For external IDs, we released Custom ID support via metafields. You can tag resources with IDs from your systems and query directly

This works for queries on products, variants, collections, customers, orders, and locations and through the productSet and customerSet mutations.

For the product/variant upsert concern, the productSet mutation was specifically designed for database sync workflows like yours. You no longer need to split variants into create/update sets or track whether resources exist. Pass everything in one call with an identifier. If your sync job fails partway through, you can safely re-run it without creating duplicates. The idempotent mechanism should give some of that Atomicity that you’re mentioning.

For context on why the product model got more complex, the 100-variant limit was the biggest blocker for medium-to-large merchants. To scale to 2,048 variants per product, options had to become first-class entities. The product model changes explain this was necessary to remove that constraint.

I do find your mention of company updates requiring 5 separate mutations interesting. B2B has only existed in GraphQL, so there’s no REST precedent, so I would like to know more context about your current workflow with these mutations so I can properly share this feedback for you.

Let me know if some of the mutations will work for you. As a long time partner, your perspective on how the API has evolved matters and I want to make sure I understand your workflows fully so I can pass this feedback along effectively.

1 Like

Hey @James_Hutchison, did the above help unblock you on some of those roadblocks you were running in to?

Kyle, thanks for the follow up, greatly appreciated.

Finely Grained

It depends - having to construct (completely) different bodies for different sets of fields, which could be served from the single endpoint is cumbersome - I accept this is a design decision.

External Ids

OK - Got it, this is will be useful.

Product Set

This is useful and something akin to the REST API, but (extremely) still problematic/has many bugs, I think.

  • The update handling seems undefined. Our understanding of GraphQL may be lacking, but it seems we can send any mutation name in the query and as long as the arguments match, it works (the below seems to work)?
    "query": "mutation BLAHBLAHVLAKS($input: ProductSetInput!, $synchro...
  • The documentation around this seems limited - there is no documentation difference mutation names in the examples.
  • When sending a payload to ‘createProductAsynchronous’ graphQL mutation, it seem seems the variants are matched by their option values? Is this correct? We could not see this in the documentation, but seems to be true.
  • 2048 Variant Limit - Great that we can create a product with 2048 variants, but we cannot get a reply consisting of the 2048 variants - it’s a 250 variant limit. As such we need to issue multiple query requests to retrieve the variant ids and inventory item ids.

Company

We can create a company record easily in GQL, but updating requires up to 5, separate mutations.

Hey @James, thanks for the detailed follow-up.

The mutation naming flexibility is standard GraphQL behavior. The operation name (the text after mutation like BLAHBLAHVLAKS) is purely a client-side label for debugging and logging. The server only cares about the mutation field name (productSet) and its arguments. This is by design in the GraphQL spec to let you name operations descriptively for your codebase. Not a bug, but a common point of confusion when coming from REST.

Regarding variant matching by option values, this is to support upsert workflows, but I agree it should be explicit in the public docs Sync product data from an external source. I’ll flag this internally for better documentation.

Next, As of API version 2025-01, single product queries support up to 2,000 variants (increased from 250). This means you can retrieve all 2,048 variants with just 2 paginated queries instead of 9. You can see this in the 2025-01 release notes under “Single product variant connection”.

For company updates, the reasoning is to allow granular permissions and atomic operations. I can see how this is tedious for ERP sync use cases where you want something like productSet for companies. Manage client company locations

The best workaround is batching multiple mutations in one HTTP request to reduce round-trip overhead. I tested this with 5 company mutations (company update, location update, address assignment, tax settings, and contact update) and confirmed it works. All 5 mutations executed in a single request. Here’s an example:

mutation BatchCompanyUpdate(
  $companyId: ID!
  $companyInput: CompanyInput!
  $locationId: ID!
  $locationInput: CompanyLocationUpdateInput!
) {
  company: companyUpdate(companyId: $companyId, input: $companyInput) {
    company { id }
    userErrors { field message }
  }
  location: companyLocationUpdate(companyLocationId: $locationId, input: $locationInput) {
    companyLocation { id }
    userErrors { field message }
  }
}

Each mutation gets its own alias and error handling, and they all execute in one HTTP call. The trade-off is that query costs accumulate (no rate limit benefit), and you need to check each mutation’s userErrors separately, but for sync workflows the latency improvement is significant.

Based on your feedback, I’ve submitted a feature request for a companySet-style mutation that would handle company + locations + contacts + tax settings in a single call with upsert logic similar to productSet. This would be especially valuable for ERP integrations and database sync workflows. I’ve included your specific use case and the context from this thread.

Let me know if you’d like me to clarify anything else.