Why GraphQL Over REST?

REST API calls return fixed data structures. GraphQL returns exactly what you ask for. This matters at scale.

Suppose you're building an inventory dashboard. REST requires three separate calls:

  • GET /products (returns all fields: ID, title, handle, vendor, status, created_at, updated_at, images, variants, collections, metafields)
  • GET /orders (returns all order data)
  • GET /customers (returns all customer fields)

GraphQL: one query, one response, your data shape.

REST forces you to either over-fetch data (wasting bandwidth) or under-fetch it (requiring follow-up calls). GraphQL eliminates this. The Shopify team measured a 60% reduction in bandwidth usage when merchants switched from REST to GraphQL for complex apps.

Another advantage: GraphQL strongly types the schema. Your IDE auto-completes. You catch errors before runtime. REST requires reading documentation and guessing field names.

The Shopify GraphQL Admin API Architecture

Shopify exposes GraphQL through two endpoints:

API Type Base URL Authentication Use Case
Admin API https://yourstore.myshopify.com/admin/api/2026-01 OAuth + access token Modify inventory, orders, customers, fulfillment. Requires scopes.
Storefront API https://yourstore.myshopify.com/api/2026-01/graphql.json Public access token Public product queries, cart operations, checkout. Limited access.

For app development, you'll primarily use the Admin API. It requires OAuth authentication through the Shopify App CLI or manual token generation.

Authentication flow:

  1. User installs your app
  2. Shopify redirects to your callback URL with a temporary code
  3. Your backend exchanges code for permanent access token
  4. Store token securely (encrypted at rest)
  5. Include token in Authorization header for all requests

Core Query Patterns

Pattern 1: Fetch a Single Product

query {
  product(id: "gid://shopify/Product/123456") {
    id
    title
    handle
    vendor
    priceRange {
      minVariantPrice {
        amount
        currencyCode
      }
    }
    images(first: 3) {
      edges {
        node {
          url
          altText
        }
      }
    }
  }
}

Notice the pagination structure: images(first: 3) returns up to 3 images. Shopify uses cursor-based pagination for large datasets.

Pattern 2: Fetch Multiple Products with Filtering

query {
  products(first: 50, query: "status:active AND created:>2025-01-01") {
    pageInfo {
      hasNextPage
      endCursor
    }
    edges {
      node {
        id
        title
        handle
        totalInventory
      }
    }
  }
}

The query parameter filters by Shopify query syntax. Common filters:

  • status:active / status:archived
  • created:>2025-01-01 / updated:<2025-01-01
  • vendor:"Nike"
  • product_type:"Apparel"

Pattern 3: Nested Queries for Order Data

query {
  orders(first: 10, sortKey: CREATED_AT, reverse: true) {
    edges {
      node {
        id
        name
        email
        totalPrice
        lineItems(first: 5) {
          edges {
            node {
              title
              quantity
              variant {
                id
                sku
              }
            }
          }
        }
        customer {
          id
          displayName
          email
        }
      }
    }
  }
}

Common Mutations

Mutation 1: Create a Product

mutation {
  productCreate(input: {
    title: "New Sneaker Drop"
    productType: "Footwear"
    vendor: "Nike"
    handle: "new-sneaker-drop-2026"
    bodyHtml: "<p>Limited edition release</p>"
  }) {
    product {
      id
      handle
    }
    userErrors {
      field
      message
    }
  }
}

Always check userErrors in mutation responses. Shopify returns validation errors here, not in HTTP status codes.

Mutation 2: Update Inventory

mutation {
  inventorySetQuantities(input: {
    reason: "CORRECTION"
    quantities: [
      {
        inventoryItemId: "gid://shopify/InventoryItem/123"
        availableQuantity: 50
      }
    ]
  }) {
    inventoryItems {
      id
      tracked
      quantity
    }
    userErrors {
      message
    }
  }
}

This is the modern way to manage inventory. The old REST PUT /inventory_levels approach is deprecated.

Mutation 3: Fulfill an Order

mutation {
  fulfillmentCreateV2(
    lineItems: {
      id: "gid://shopify/LineItem/456"
      quantity: 2
    }
    trackingInfo: {
      number: "1Z999AA10123456784"
      company: UPS
    }
    notifyCustomer: true
  ) {
    fulfillment {
      id
      status
    }
    userErrors {
      message
    }
  }
}

Pagination Pitfall: Cursor-Based Navigation

Shopify doesn't support offset-based pagination. You must use cursors.

Wrong approach:

query {
  products(first: 50, offset: 100) {  # offset doesn't exist
    edges { node { id } }
  }
}

Right approach:

query {
  products(first: 50, after: "eyJkaXJlY3Rpb24iOiJuZXh0IiwibGFzdF9pZCI6IjEyMzQ1In0=") {
    pageInfo {
      hasNextPage
      endCursor
    }
    edges {
      node { id }
    }
  }
}

Store endCursor from the previous response. Pass it as after for the next page. This scales to millions of records without performance degradation.

Rate Limiting: API Bucket Model

Shopify doesn't throttle requests per second. Instead, you get a "bucket" of 40 points per second.

Operation Cost Impact
Simple query (e.g., fetch product title) 1 point 40 simple queries/second
Medium query (nested fields, 10 objects) 3-5 points 8-10 queries/second
Complex mutation (update inventory, assets) 10+ points 4 mutations/second
Bulk query (1000+ items) 100 points Use bulk operations

If you hit the bucket limit, Shopify returns 429 Throttled with a Retry-After header. Most APIs return 20 points, Shopify admin returns 40—meaning it's deliberately generous to encourage GraphQL adoption over REST.

Optimization: Use bulk operations for large batches. Instead of looping through 10,000 products with individual queries, submit a JSONL file to the bulk operations API. Shopify processes it asynchronously and webhooks you the results.

Tenten's API-First Approach

Most Shopify apps treat the API as a way to read data and push updates. Tenten's clients build apps with the GraphQL API at the core—querying it repeatedly for fresh insights, feeding them into custom logic, and automating complex workflows.

Example: A client built an inventory allocation engine that queries variants in real-time, checks warehouse stock via webhook, and auto-generates purchase orders to suppliers. No Shopify UI involved. All GraphQL → custom backend → email.

The efficiency gain: from 3 hours of manual work per day to fully automated. The cost to build: under $8K.

This is the power of GraphQL. It's not faster just because it returns less data. It's faster because it lets you build workflows that REST APIs can't support without dozens of integration points.

Production Best Practices

1. Implement Exponential Backoff

import time
import random

def query_with_backoff(query_str, max_retries=5):
    for attempt in range(max_retries):
        response = make_graphql_request(query_str)
        
        if response.status_code == 429:
            wait_time = 2 ** attempt + random.uniform(0, 1)
            time.sleep(wait_time)
            continue
        
        return response
    
    raise Exception("Max retries exceeded")

Don't just sleep for a fixed time. Exponential backoff with jitter prevents thundering herd scenarios.

2. Cache Strategically

Cache product data for 5-15 minutes. Inventory and orders change frequently—cache for 30-60 seconds. Customer data rarely changes—cache for 24 hours.

@cache_result(ttl_seconds=300)
def fetch_product(product_id):
    return query_shopify(f"product/{product_id}")

3. Use Bulk Operations for Batch Work

If you're processing 5K+ items, use the bulk operations API. Submit a JSONL file, Shopify queues it, and sends results via webhook. Zero rate limiting applies.

4. Validate Input, Always

GraphQL strongly types the schema. But your app input doesn't. Validate customer input against the schema before sending.

def validate_product_input(title, vendor, product_type):
    if not title or len(title) > 255:
        raise ValueError("Title required, max 255 chars")
    if not vendor or len(vendor) > 255:
        raise ValueError("Vendor required, max 255 chars")
    return True

When to Use REST vs GraphQL

Use GraphQL:

  • Complex queries requiring multiple fields from nested objects
  • Batch operations on 50+ items
  • Custom app logic that benefits from strong typing
  • Reducing bandwidth (important for mobile apps)

Use REST:

  • Simple CRUD operations on single resources
  • Webhooks (still REST-based)
  • Legacy apps where GraphQL migration isn't worth the effort

For new apps in 2026, start with GraphQL. The ecosystem is mature, client libraries are solid, and the performance advantage compounds.

Key Takeaways

GraphQL shifts the burden from the API to the client—you ask for exactly what you need. This requires learning cursor-based pagination, the rate-limiting bucket model, and best practices around caching and backoff. But once you internalize these patterns, building complex workflows becomes significantly faster.

The Shopify GraphQL Admin API is production-ready. Use it for new apps.

Frequently Asked Questions

What's the difference between the Shopify GraphQL Admin API and Storefront API?

The Admin API manages inventory, orders, customers, and fulfillment. It requires OAuth authentication and merchant app installation. The Storefront API is public and only queries product data visible to customers. Use Admin for operational apps, Storefront for storefront embeds and third-party integrations.

Can I use GraphQL to create a custom checkout flow?

Partially. GraphQL can modify orders and create draft orders, but checkout customization now lives in Remix frameworks and Hydrogen. For custom checkout, use the Checkout UI extension API, not GraphQL directly.

How do I handle pagination in GraphQL?

Shopify uses cursor-based pagination. Query the endCursor from pageInfo, then pass it as the after parameter in the next request. There is no offset pagination.

What happens if my app exceeds the rate limit?

Shopify returns a 429 Throttled response with a Retry-After header. Implement exponential backoff. The rate limit is per app, not per merchant—so all stores using your app share the same bucket.

Is it cheaper to use GraphQL than REST from a data transfer perspective?

Yes. GraphQL returns only requested fields, reducing payload size by 40-60% on average. This is especially important for mobile-first apps or merchants with poor connectivity.

Can I bulk import products via GraphQL?

Use the bulk operations API. Submit a JSONL file with product data, Shopify processes it asynchronously, and webhooks the results. This bypasses rate limiting entirely.

What authentication method does the GraphQL API use?

OAuth token-based. When a merchant installs your app, Shopify issues an access token scoped to the app's requested permissions. Include the token in the X-Shopify-Access-Token header or as a Bearer token in the Authorization header.