Shopify’s GraphQL Admin API is the primary interface for building integrations, custom apps, and automation workflows for Shopify stores. While the REST Admin API still works, Shopify has made it clear that GraphQL is the future: all new features land in GraphQL first, and some resources are GraphQL-exclusive.
If you are coming from REST APIs, GraphQL requires a different mental model. Instead of hitting predefined endpoints that return fixed data shapes, you write queries that request exactly the fields you need. This is more powerful but has a steeper learning curve. This guide bridges that gap with practical examples from our Shopify development work.
Why Shopify is moving to GraphQL
GraphQL solves several problems that REST APIs struggle with in the context of a complex commerce platform:
- Over-fetching — REST endpoints return fixed data shapes. If you need a product’s title and price, the REST endpoint returns everything. GraphQL lets you request exactly what you need.
- Under-fetching — to get a product with its variants, images, and metafields via REST requires multiple API calls. In GraphQL, a single query fetches all related data.
- API versioning — GraphQL’s type system makes it easier to evolve the API without breaking existing queries.
- Rate limiting precision — cost-based rate limiting is fairer than request-based limiting, as simple queries cost less than complex ones.
If you are starting a new Shopify integration today, use GraphQL. The investment in learning the query language pays for itself in fewer API calls, less data processing, and access to the latest platform features.

Query fundamentals
A GraphQL query is a structured request for specific data. Here is a basic query that fetches the first five products with their titles and prices:
query {
products(first: 5) {
edges {
node {
id
title
status
priceRangeV2 {
minVariantPrice {
amount
currencyCode
}
}
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
Understanding connections and edges
Shopify’s GraphQL API uses the Relay connection pattern for paginated lists. Resources that return lists use a connection → edges → node structure. The edges array wraps each result with a cursor for pagination, while node contains the actual data object. The pageInfo object provides pagination metadata.
Variables and query parameterisation
query GetProduct($handle: String!) {
productByHandle(handle: $handle) {
id
title
descriptionHtml
images(first: 5) {
edges {
node { url altText width height }
}
}
variants(first: 50) {
edges {
node {
id title price availableForSale
selectedOptions { name value }
}
}
}
}
}
// Variables (passed as JSON)
{ "handle": "classic-leather-belt" }
Executing queries in Node.js
import { shopifyApi } from '@shopify/shopify-api';
const shopify = shopifyApi({
apiKey: process.env.SHOPIFY_API_KEY,
apiSecretKey: process.env.SHOPIFY_API_SECRET,
scopes: ['read_products'],
hostName: process.env.HOST,
apiVersion: '2026-01',
});
const session = await getSession(request);
const client = new shopify.clients.Graphql({ session });
const response = await client.request(`
query GetProducts($first: Int!) {
products(first: $first) {
edges { node { id title } }
}
}
`, { variables: { first: 10 } });
const products = response.data.products.edges.map(edge => edge.node);
Mutations: creating and updating data
Mutations modify data. Unlike REST where you use different HTTP methods (POST, PUT, DELETE), GraphQL uses named mutations for all write operations:
mutation CreateProduct($input: ProductInput!) {
productCreate(input: $input) {
product { id title handle }
userErrors { field message }
}
}
// Variables
{
"input": {
"title": "Organic Cotton T-Shirt",
"bodyHtml": "<p>Made from 100% organic cotton.</p>",
"vendor": "Our Brand",
"productType": "T-Shirts",
"tags": ["organic", "cotton", "basics"],
"status": "DRAFT"
}
}
Always check userErrors
Shopify mutations return userErrors instead of throwing exceptions. A mutation can return HTTP 200 with errors in the response body. Always check for errors after every mutation:
const response = await client.request(MUTATION, { variables: input });
if (response.data.productCreate.userErrors.length > 0) {
const errors = response.data.productCreate.userErrors;
errors.forEach(error => {
console.error(`Field: ${error.field}, Message: ${error.message}`);
});
throw new Error('Product creation failed: ' + errors[0].message);
}
const product = response.data.productCreate.product;

Cursor-based pagination
Cursor-based pagination is more reliable than offset-based pagination because it handles concurrent data changes correctly. This is the standard pattern for fetching all records, as documented in the Shopify API guide:
async function fetchAllProducts(client) {
const products = [];
let hasNextPage = true;
let cursor = null;
while (hasNextPage) {
const response = await client.request(`
query GetProducts($first: Int!, $after: String) {
products(first: $first, after: $after) {
edges { node { id title handle } }
pageInfo { hasNextPage endCursor }
}
}
`, { variables: { first: 50, after: cursor } });
const { edges, pageInfo } = response.data.products;
products.push(...edges.map(e => e.node));
hasNextPage = pageInfo.hasNextPage;
cursor = pageInfo.endCursor;
}
return products;
}
Cost-based rate limiting
The GraphQL Admin API uses a leaky bucket algorithm with cost-based throttling. Your bucket holds 1,000 points and refills at 50 points per second:
// The response includes throttle information
{
"extensions": {
"cost": {
"requestedQueryCost": 12,
"actualQueryCost": 8,
"throttleStatus": {
"maximumAvailable": 1000,
"currentlyAvailable": 992,
"restoreRate": 50
}
}
}
}
// Use this to implement proactive backoff
async function queryWithRateLimit(client, query, variables) {
const response = await client.request(query, { variables });
const { throttleStatus } = response.extensions.cost;
if (throttleStatus.currentlyAvailable < 100) {
const waitMs = (100 - throttleStatus.currentlyAvailable) / throttleStatus.restoreRate * 1000;
await new Promise(resolve => setTimeout(resolve, waitMs));
}
return response;
}
Bulk operations for large datasets
For operations that need to process thousands of records, use the Bulk Operations API. It runs asynchronously and returns results via a downloadable JSONL file:
mutation {
bulkOperationRunQuery(
query: """
{
products {
edges {
node {
id title
variants { edges { node { id sku inventoryQuantity } } }
}
}
}
}
"""
) {
bulkOperation { id status }
userErrors { field message }
}
}
# Poll for completion
query {
currentBulkOperation {
id status errorCode objectCount
url # JSONL download URL when complete
}
}
Error handling patterns
GraphQL error handling differs from REST. There are three types of errors to handle:
// 1. Network errors (request never reached Shopify)
try {
const response = await client.request(query, { variables });
} catch (networkError) {
// Retry with exponential backoff
}
// 2. GraphQL errors (query syntax or execution errors)
if (response.errors) {
response.errors.forEach(error => {
console.error(`GraphQL error: ${error.message}`);
if (error.extensions?.code === 'THROTTLED') {
// Rate limited - wait and retry
}
});
}
// 3. User errors (business logic validation failures)
if (response.data?.productCreate?.userErrors?.length > 0) {
// Handle validation errors
}

Development tooling
- Shopify GraphiQL Explorer — built into the admin at
/admin/api/2026-01/graphql.json. Provides autocomplete, documentation, and query history. - Shopify CLI —
shopify app devprovides an authenticated GraphQL explorer for app development. - GraphQL Code Generator — generates TypeScript types from your queries for type-safe API interactions.
- Insomnia / Postman — both support GraphQL with variable support and query formatting.
Migrating from REST to GraphQL
If you have existing REST integrations, migrate incrementally. Start with new features in GraphQL and migrate existing endpoints as maintenance requires:
// REST equivalent
// GET /admin/api/2026-01/products/123456.json
// GraphQL equivalent
query {
product(id: "gid://shopify/Product/123456") {
id title handle status
variants(first: 50) {
edges { node { id title sku price } }
}
}
}
// Key differences:
// 1. IDs are global IDs (gid://shopify/Product/123456)
// 2. You select only the fields you need
// 3. Lists use cursor-based pagination
// 4. Nested resources are fetched in a single request

Production best practices
- Always use variables — never interpolate values into query strings. Variables prevent injection and enable query caching.
- Request only what you need — every additional field increases query cost and response size.
- Handle rate limits gracefully — use the throttle status to implement proactive backoff.
- Use bulk operations for large datasets — never paginate through thousands of records one page at a time.
- Version your API calls — always specify the API version explicitly.
- Log query costs — track actual query costs over time to identify optimisation opportunities.
- Use fragments for shared fields — define GraphQL fragments for field sets you use across multiple queries.
- Implement idempotency — use mutation input IDs where available to prevent duplicate operations on retries.
The Shopify GraphQL API is a powerful tool for building sophisticated integrations. The learning curve is steeper than REST, but the payoff in efficiency, capability, and future-proofing is substantial. If you need help building GraphQL integrations for your Shopify store, get in touch — it is core to our Shopify development work. We also cover related topics in our Storefront API guide.
