Shopify: Unlocking the power of Shopify's new Product Model & APIs
Intro
There have been a lot of recent changes related to managing product information with Shopify's APIs, particularly in how app developers now interact with product data. These include changes to the product model, how products themselves work, and migrating from REST to GraphQL.
As an developer who has experience building with the new Product APIs model, I'd like to share my experiences, and unpack some real-world examples that will help with understanding the updates and migration.
Product Model
Let's go over the product model changes first, as that will help us understand why GraphQL operates the way it does.
Previously the product model was compact with Product, Images, Product Variants, and Inventory Items all co-located together. The images were tied to Products and Variants so could not be reused between products in any file/media gallery.
Inventory Item information such as SKU and Weight were all combined with Product Variants. And of course, Product Variants were always attached to Products meaning we could only ever have 200 variants.
This made it easy to work with from a developer perspective, as you got all data in one API call, but you can see how it wasn’t easily extensible or very future proof.
The new product model separates these key components of a product into something that looks like the image below:
What you'll notice is that this also matches the Shopify Admin layout of a product, with a separate page for product variants, and separate boxes for media and inventory within that. Remembering the Shopify Admin Product layout will be useful when we come to looking at the GraphQL Product APIs.
The changes to the model give us some great capabilities:
- Up to 2048 variants.
- Media can be shared between products without copying the file multiple times, which makes it easier for merchants to manage
- Support for larger videos and 3D Models as well as images.
- Separation of Product and Product Variant merchandising information from Inventory and Operational data.
You can learn more about how to use the latest GraphQL product APIs in the developer documentation, which describes the model and components and some of the reasons behind the changes, and hopefully you can see some of the opportunities this update unlocks for us as developers to do a lot more with products.
GraphQL
Let's dive into some GraphQL now. If you’re new to GraphQL I'd recommend checking out the Shopify developer documentation first, which also includes a detailed migration guide too.
Rate Limits
One important aspect to understand when migrating from REST to GraphQL is that the rate limits are quite different. You may well need to make more calls in GraphQL than REST, but the rate limits are calculated based on a points system.
Points are determined either by how much each query costs, or with mutations that will always cost 10 points, regardless of the changes being made. You have a lot more points in GraphQL to use, compared to REST limits, so don't worry too much if you need to make additional GraphQL requests compared to REST.
You can also use Bulk Operations in GraphQL to create or update multiple products and variants in one go or process larger volumes of data.
Creation
The first thing we'll want to do with any product is create it. How exactly you do this will depend on how you want to work.
You can first create just the top level product information using productCreate, and if you're starting from scratch I would recommend this simpler option. You can see an example of what this would look like below:
const query = `mutation createProduct($product: ProductCreateInput!) {
productCreate(product: $product) {
product {
id
}
userErrors {
field
message
}
}
}`
const variables = {
"product": {
"title": "Winter hat",
"productOptions": [
{
"name": "Color",
"values": [
{ "name": "Grey" },
{ "name": "Black" }
]
}
]
}
}
Alternatively you could use productSet which allows you to create a more complete product with variants etc., which is easier if you are migrating from an existing app or REST integration. I’ll give an example of this below given the complexity involved here, where we create a product, variants and set weights and inventory information.
One important thing to note here is that productSet is an upsert endpoint, it will create or update a product so just be aware of this, particularly if you are only updating variant information.
The first thing to call out from the example is we have a synchronous input option here, if you are creating a large product with lots of files and variants, it is best to run this as not synchronous otherwise it may timeout. You can then query the productSetOperation to check when this has completed.
As always with GraphQL ensure you check the userErrors in your response to catch any issues with your input. You can of course run this as a bulk job if you want to create a lot of products in one go.
On the input here is an example creating a product with a variant, media item (file) and inventory information.
const query = `
mutation createProductAsynchronous($productSet: ProductSetInput!, $synchronous: Boolean!) {
productSet(synchronous: $synchronous, input: $productSet) {
product {
id
}
productSetOperation {
id
status
userErrors {
code
field
message
}
}
userErrors {
code
field
message
}
}
}`
const variables = {
"synchronous": false,
"productSet": {
"title": "Winter hat",
"metafields": [
{
"description": "<your-description>",
"key": "<your-key>",
"namespace": "<your-namespace>",
"type": "<your-type>",
"value": "<your-value>"
}
],
"productOptions": [
{
"name": "Color",
"position": 1,
"values": [
{
"name": "Grey"
},
{
"name": "Black"
}
]
}
],
"variants": [
{
"optionValues": [
{
"optionName": "Color",
"name": "Grey"
}
],
"price": 79.99,
"inventoryItem": {
"measurement": {
"weight": {
"value": 1.70,
"unit": "GRAMS"
}
},
"sku": "SKU-123",
"tracked": true,
}
},
{
"optionValues": [
{
"optionName": "Color",
"name": "Black"
}
],
"price": 69.99,
"inventoryItem": {
"measurement": {
"weight": {
"value": 1.70,
"unit": "GRAMS"
}
},
"sku": "SKU-456",
"tracked": true,
}
}
]
}
}
Variants
If you’ve used productCreate and want to then create some variants and media, you can then use the productVariantsBulkCreate which will allow you to create the variants and media items on a product.
Personally I would recommend using productCreate and this method, instead of productSet, to manage the product and variants as it keeps everything a little easier to understand. The input data is less complex, there is less risk as these are not upsert endpoints and you have to store less identifiers than needed with productSet. I believe it also sets you up nicely for updates as they follow a similar format.
const query = `mutation productVariantsCreate($productId: ID!, $variants: [ProductVariantsBulkInput!]!, $media: [CreateMediaInput!]) {
productVariantsBulkCreate(productId: $productId, variants: $variants, media: $media) {
productVariants {
id
title
selectedOptions {
name
value
}
}
userErrors {
field
message
}
}
}`
const variables = {
"productId": "gid://shopify/Product/20995642",
"variants": [
{
"price": 15.99,
"compareAtPrice": 19.99,
"optionValues": [
{
"name": "Golden",
"optionId": "gid://shopify/ProductOption/328272167"
}
]
}
],
"media": [
{
"mediaContentType": "IMAGE",
"originalSource": "someExternalURL.jpg"
},
{
"mediaContentType": "VIDEO",
"originalSource": "stagedMediaResourceURL"
}
]
}
Updates
Once you have a product, you’re going to want to update it or the variants. I'll cover updating media in the next section.
Again, you can use productSet here, as it will update the product. Make sure if you are doing this you provide IDs for all the existing variants, files etc. otherwise you will end up with duplicates of variants or other product content.
Alternatively you can use the specific methods, again this is what I would recommend, we’ve got productUpdate, productVariantsBulkUpdate which can allow you to really easily update certain properties on the product.
An example of updating a product and adding media.
const query = `mutation updateProductData($input: ProductInput!, $media: [CreateMediaInput!]) {
productUpdate(input: $input, media: $media) {
product {
id
}
userErrors {
field
message
}
}
}`
const variables = {
"input": {
"id": "gid://shopify/Product/912855135",
"descriptionHtml": "Updated description"
},
"media": [
{
"mediaContentType": "IMAGE",
"originalSource": "someExternalURL.jpg"
},
{
"mediaContentType": "VIDEO",
"originalSource": "stagedMediaResourceURL"
}
]
}
There are also some more specific named methods, if you are looking to change a product's options or published status for example.
An example of an existing product with colour options, now adding size options and automatically creating the new variants. If the product didn’t have the size options when initially created.
const query = `mutation createAdditionalOptions($productId: ID!, $options: [OptionCreateInput!]!, $variantStrategy: ProductOptionCreateVariantStrategy) {
productOptionsCreate(productId: $productId, options: $options, variantStrategy: $variantStrategy) {
userErrors {
field
message
code
}
}
}`
const variables = {
"productId": "gid://shopify/Product/20995642",
"variantStrategy": "CREATE",
"options": [
{
"name": "Size",
"values": [
{ "name": "Small" },
{ "name": "Medium" },
{ "name": "Large" }
]
}
]
}
Managing Media
The developer documentation comprehensively describes how to manage media for a product, so I won't repeat what is outlined there, but there are a couple of things I think are important to highlight:
- Previously, REST calls only supported one image at a time. With the GraphQL methods, you can upload multiple images and other media in the same productUpdate call.
- You can also stage your media so multiple images, videos and 3D models can be added at the same time, making it more efficient for adding multiple media items at once.
- You can also use bulk imports with productUpdate, if you want to update multiple products and media.
- If you're adding multiple media/images at once in a single GraphQL mutation, this will still only cost 10 points towards rate limits.
- You can still include an external link to an image, when adding it to a product or variant. However, this normally means you have to host it somewhere first. Now you can use stagedUploadsCreate so you don’t need to do that anymore, and you can upload multiple images/videos or 3D items in one go!
Inventory Item
Up until now we've been dealing with the product or product variant information, which is mostly around merchandising the product and preparing it for sale. This is where the inventory item comes into play, it represents the actual physical item that will be sold.
Here is where the Stock Keeping Unit (SKU), weight or other logistical information all exist and is connected to the various inventory levels at each location. You can set some of these properties when creating a variant as we discussed previously. However it does also have its own methods as well, for example updating an inventory item.
An example of updating the information.
const query = `mutation inventoryItemUpdate($id: ID!, $input: InventoryItemInput!) {
inventoryItemUpdate(id: $id, input: $input) {
inventoryItem {
id
}
userErrors {
field
message
}
}
}`
const variables = {
"id": "gid://shopify/InventoryItem/43729076",
"input": {
"cost": 145.89,
"tracked": false,
"countryCodeOfOrigin": "US",
"provinceCodeOfOrigin": "OR",
"harmonizedSystemCode": "621710",
"sku": "SKU-123",
"measurement": {
"weight": {
"value": 1.70,
"unit": "GRAMS"
}
}
}
}
InventoryItem also has its own webhooks, so if you only need these pieces of information, for example for a warehouse, you can subscribe to the inventory webhook instead of relying on the product webhooks, which should be a bit more efficient.
Webhooks
If you're not using webhooks yet, they're a useful tool to get information out of Shopify asynchronously without rate limits. There are some useful docs here on them if you’d like further background https://shopify.dev/docs/apps/build/webhooks
Product Webhooks will now ONLY include the first 100 variants, all additional variants will be listed in the “variant_ids” property, this will also be backdated onto all product webhook versions. This will inform you which variants have been updated, you can then use the ID to more easily get the product variant information from Shopify API.
Also, there is the opportunity here to move beyond just product webhooks as there are also webhooks for the inventory item updates, inventory level updates, variants in and out of stock. So, make sure you check the documentation if there is an alternative that works for you!
Example Use Case
Let's unpack a full real world scenario. For example, let's imagine you have a products' SKU, and you want to update its inventory information and publication status, which is a common process.
This is how I would approach the problem and determine which GraphQL calls I may need:
It’s a SKU so it's Inventory Item which is related to a product variant (we know that from where the input is located in the Shopify Admin), so find we can that query and see if I can search by SKU, which we can https://shopify.dev/docs/api/admin-graphql/2025-01/queries/productVariants or https://shopify.dev/docs/api/admin-graphql/2025-01/queries/inventoryItems
From either query we can return a list of product variants on the nodes, so we need to look at that to see how we can get to the inventory and publication https://shopify.dev/docs/api/admin-graphql/2025-01/objects/ProductVariant
We can see on the variant it has Inventory Item which it says is for managing inventory, so we’ll probably need that ID later https://shopify.dev/docs/api/admin-graphql/2025-01/objects/ProductVariant#field-inventoryitem
But there’s nothing on there about the publication, so that must be on the product also that is where publications are on the Shopify Admin, there’s a load of fields on there about publication so grab the product ID which we'll probably need later as well https://shopify.dev/docs/api/admin-graphql/2025-01/objects/ProductVariant#field-product
From there we know we are now looking to make inventory changes, which will be a mutation and related to an inventory item. In the inventory section of the GraphQL docs mutations there is inventoryItemUpdate but that does have any quantities on, so let’s go with the one that has quantities in the name, which looks right and needs the ID we grabbed earlier https://shopify.dev/docs/api/admin-graphql/2025-01/mutations/inventoryAdjustQuantities
We saw before that the publication is on the product, so look through the product mutations for anything referencing it. There’s product publish, which sounds right and needs the product id from before, it is however deprecated but does direct you to the correct method of publishablePublish (my personal favourite mutation name) https://shopify.dev/docs/api/admin-graphql/2025-01/mutations/publishablePublish
We need to do multiple calls here which is fine, or if you want to you can combine mutations if you are doing them regularly. An example of how you can do this below, make sure each mutation has a unique alias. For example I'm calling mine detaching and appending.
const query = `mutation productVariantManageMedia(
$productId: ID!,
$detach: [ProductVariantDetachMediaInput!]!,
$append: [ProductVariantAppendMediaInput!]!) {
detaching: productVariantDetachMedia(productId: $productId, variantMedia: $variantMedia) {
userErrors {
code
field
message
}
}
appending: productVariantAppendMedia(productId: $productId, variantMedia: $variantMedia) {
userErrors {
code
field
message
}
}
}`
If you're using NodeJS you're in luck because Shopify libraries are great. This is a super lightweight GraphQL client for it @shopify/admin-api-client If you're moving between REST IDs and GraphQL IDs then you can always get the legacyResourceId from the Admin API.
Lastly use the Shopify CLI and @shopify/api-codegen-preset to generate types from your GraphQL queries which makes working with the data much much easier.
Conclusion
I hope that this has helped add some context with real world examples and approaches to tackling the recent changes to product models and API’s. Once you’ve migrated to GraphQL, you're set up for the future, as this is Shopify’s primary API format now.
Which means upgrading and keeping track of any changes should be simpler for you and your apps in the future. Hopefully you can also see the opportunities that the product changes unlock for us to develop even more powerful product apps.