GraphQL Essentials for Shopify
GraphQL is the primary language for communicating with Shopify's APIs. If you have only used REST APIs before, GraphQL will feel different -- you describe exactly what data you want, and the server returns precisely that shape. No over-fetching, no under-fetching, no chaining multiple requests together. This lesson teaches you GraphQL from the ground up, with Shopify-specific patterns you will use every day.
Why GraphQL Over REST
Consider fetching a product with its variants, images, and metafields. With REST, you might need:
GET /admin/api/2026-01/products/123.json → product data
GET /admin/api/2026-01/products/123/variants.json → variant data
GET /admin/api/2026-01/products/123/images.json → image data
GET /admin/api/2026-01/products/123/metafields.json → metafield data
That is four HTTP requests, each returning fields you might not need. With GraphQL:
query {
product(id: "gid://shopify/Product/123") {
title
status
variants(first: 10) {
edges {
node {
title
price
inventoryQuantity
}
}
}
images(first: 5) {
edges {
node {
url
altText
}
}
}
metafields(first: 10) {
edges {
node {
namespace
key
value
}
}
}
}
}
One request. Exactly the fields you need. This is why Shopify is going all-in on GraphQL.
Queries: Reading Data
Queries are how you read data from Shopify. Every query starts with a root field and drills into the data graph.
Basic Query Structure
query ProductList {
products(first: 10, query: "status:active") {
edges {
node {
id
title
handle
status
totalInventory
priceRangeV2 {
minVariantPrice {
amount
currencyCode
}
maxVariantPrice {
amount
currencyCode
}
}
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
Key concepts:
- Operation name (
ProductList): Optional but recommended -- helps with debugging and logging - Arguments (
first: 10, query: "status:active"): Filter and paginate results - Selection set: The nested fields you want returned
- Edges and nodes: Shopify uses the Relay connection pattern for lists
- pageInfo: Pagination metadata for cursor-based paging
Using Variables
Never interpolate values directly into query strings. Use variables instead:
query ProductById($id: ID!) {
product(id: $id) {
title
description
vendor
productType
tags
createdAt
}
}
{
"variables": {
"id": "gid://shopify/Product/7654321"
}
}
Variables prevent GraphQL injection attacks, enable query caching, and make your code cleaner. The Shopify Admin GraphQL client in the Remix template supports variables natively.
Common Query Patterns
Fetch orders with line items and fulfillment status:
query RecentOrders($first: Int!) {
orders(first: $first, sortKey: CREATED_AT, reverse: true) {
edges {
node {
id
name
displayFinancialStatus
displayFulfillmentStatus
totalPriceSet {
shopMoney {
amount
currencyCode
}
}
lineItems(first: 50) {
edges {
node {
title
quantity
variant {
sku
}
}
}
}
customer {
displayName
email
}
}
}
}
}
Search customers with filters:
query SearchCustomers($query: String!) {
customers(first: 25, query: $query) {
edges {
node {
id
displayName
email
ordersCount
totalSpent
tags
addresses {
city
province
country
}
}
}
}
}
Check inventory levels across locations:
query InventoryLevels($variantId: ID!) {
productVariant(id: $variantId) {
title
sku
inventoryItem {
inventoryLevels(first: 10) {
edges {
node {
location {
name
}
quantities(names: ["available", "committed", "on_hand"]) {
name
quantity
}
}
}
}
}
}
}
Mutations: Writing Data
Mutations modify data. They follow a consistent pattern in Shopify's API: you pass an input object and the mutation returns the modified resource plus any user errors.
Creating a Product
mutation CreateProduct($input: ProductInput!) {
productCreate(input: $input) {
product {
id
title
handle
status
variants(first: 5) {
edges {
node {
id
price
}
}
}
}
userErrors {
field
message
}
}
}
{
"variables": {
"input": {
"title": "Premium Widget",
"descriptionHtml": "<p>A high-quality widget for discerning customers.</p>",
"vendor": "WidgetCo",
"productType": "Widgets",
"tags": ["premium", "new-arrival"],
"status": "DRAFT",
"variants": [
{
"price": "29.99",
"sku": "WIDGET-001",
"inventoryQuantities": [
{
"availableQuantity": 100,
"locationId": "gid://shopify/Location/1"
}
]
}
]
}
}
}
Shopify mutations return a userErrors array instead of throwing exceptions. A mutation can return HTTP 200 with a successful GraphQL response but still have userErrors. Always check this array before assuming the operation succeeded.
Updating an Order Tag
mutation AddOrderTags($id: ID!, $tags: [String!]!) {
tagsAdd(id: $id, tags: $tags) {
node {
... on Order {
id
tags
}
}
userErrors {
field
message
}
}
}
Metafield Mutations
mutation SetMetafields($metafields: [MetafieldsSetInput!]!) {
metafieldsSet(metafields: $metafields) {
metafields {
id
namespace
key
value
}
userErrors {
field
message
}
}
}
{
"variables": {
"metafields": [
{
"ownerId": "gid://shopify/Product/123",
"namespace": "custom",
"key": "warranty_years",
"value": "3",
"type": "number_integer"
}
]
}
}
Subscriptions: Real-Time Updates
GraphQL subscriptions let your app receive real-time updates via webhooks. While Shopify does not support WebSocket-based subscriptions, it uses webhook subscriptions registered through GraphQL:
mutation CreateWebhookSubscription {
webhookSubscriptionCreate(
topic: ORDERS_CREATE
webhookSubscription: {
callbackUrl: "https://your-app.com/webhooks/orders-create"
format: JSON
}
) {
webhookSubscription {
id
topic
endpoint {
... on WebhookHttpEndpoint {
callbackUrl
}
}
}
userErrors {
field
message
}
}
}
Available topics include ORDERS_CREATE, PRODUCTS_UPDATE, CUSTOMERS_CREATE, INVENTORY_LEVELS_UPDATE, and dozens more.
The GraphQL Explorer
Shopify provides an interactive GraphQL explorer for testing queries:
- Go to your Partner Dashboard
- Select an app, then API access
- Click GraphiQL to open the explorer
Or use the Shopify CLI:
# Open GraphiQL for your dev store
shopify app dev --graphiql
The explorer provides:
- Schema documentation: Browse all types, fields, and arguments
- Autocomplete: Type and get suggestions
- Query validation: Errors highlighted before execution
- Variable editor: Test with different inputs
- Response viewer: Formatted JSON output
Instead of manually writing queries in GraphiQL, ask Claude Code: "Write a GraphQL query to fetch the 5 most recent orders with their line items, customer email, and fulfillment status." Claude Code understands Shopify's schema and will generate correct, efficient queries.
Bulk Operations
When you need to process thousands or millions of records, individual queries are too slow. Shopify's Bulk Operations API lets you run a query against your entire dataset asynchronously.
mutation BulkExportProducts {
bulkOperationRunQuery(
query: """
{
products {
edges {
node {
id
title
handle
status
variants {
edges {
node {
id
sku
price
inventoryQuantity
}
}
}
}
}
}
}
"""
) {
bulkOperation {
id
status
url
}
userErrors {
field
message
}
}
}
The bulk operation runs asynchronously. Poll for completion:
query BulkOperationStatus {
currentBulkOperation {
id
status
objectCount
url
errorCode
fileSize
}
}
When status is COMPLETED, the url field contains a link to a JSONL file with all results. Each line is a JSON object, and nested objects reference their parent via a __parentId field.
{"id":"gid://shopify/Product/1","title":"Widget A","handle":"widget-a","status":"ACTIVE"}
{"id":"gid://shopify/ProductVariant/10","sku":"WA-001","price":"29.99","__parentId":"gid://shopify/Product/1"}
{"id":"gid://shopify/Product/2","title":"Widget B","handle":"widget-b","status":"DRAFT"}
{"id":"gid://shopify/ProductVariant/20","sku":"WB-001","price":"19.99","__parentId":"gid://shopify/Product/2"}
Use bulk operations when you need to export or process more than a few hundred records. Common use cases: data migration, reporting, syncing with external systems, and initial data loads. Bulk operations are not subject to normal rate limits.
Error Handling
GraphQL errors in Shopify come in two forms:
1. GraphQL Errors (query-level)
These indicate problems with the query itself -- syntax errors, invalid fields, authentication failures:
{
"errors": [
{
"message": "Field 'nonExistentField' doesn't exist on type 'Product'",
"locations": [{ "line": 3, "column": 5 }]
}
]
}
2. User Errors (business logic)
These indicate valid queries that failed business rules:
{
"data": {
"productCreate": {
"product": null,
"userErrors": [
{
"field": ["input", "title"],
"message": "Title can't be blank"
}
]
}
}
}
Always handle both types:
const response = await admin.graphql(query, { variables });
const { data, errors } = await response.json();
// Check for query-level errors
if (errors) {
console.error("GraphQL errors:", errors);
throw new Error(errors[0].message);
}
// Check for business logic errors
const result = data.productCreate;
if (result.userErrors.length > 0) {
console.error("User errors:", result.userErrors);
throw new Error(result.userErrors[0].message);
}
// Success
return result.product;
Key Takeaways
- GraphQL lets you fetch exactly the data you need in a single request
- Use variables instead of string interpolation for safety and caching
- Always check userErrors in mutation responses
- Use bulk operations for large data sets (hundreds of records or more)
- The Relay connection pattern (edges/nodes/pageInfo) is used for all lists
- Claude Code can generate Shopify GraphQL queries from natural language descriptions
Next, we explore Liquid -- Shopify's templating language that powers Online Store 2.0 themes.