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:
- User installs your app
- Shopify redirects to your callback URL with a temporary code
- Your backend exchanges code for permanent access token
- Store token securely (encrypted at rest)
- 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:archivedcreated:>2025-01-01/updated:<2025-01-01vendor:"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.