Shopify API Guide for Remix Apps
Complete reference for Shopify APIs using @shopify/shopify-app-remix .
Quick Reference
API Use Case Auth Method
Admin GraphQL All backend CRUD operations authenticate.admin()
Admin REST Legacy endpoints, specific features authenticate.admin()
Storefront API Public storefront, cart, checkout Public access token
Authentication
Admin API (Loaders & Actions)
// app/routes/app.products.tsx import { json } from "@remix-run/node"; import type { LoaderFunctionArgs, ActionFunctionArgs } from "@remix-run/node"; import { authenticate } from "../shopify.server";
export const loader = async ({ request }: LoaderFunctionArgs) => { const { admin, session } = await authenticate.admin(request); // admin.graphql() - GraphQL queries // admin.rest - REST API // session.shop - current shop domain // session.accessToken - access token return json({ shop: session.shop }); };
export const action = async ({ request }: ActionFunctionArgs) => { const { admin } = await authenticate.admin(request); // Handle POST/PUT/DELETE return json({ success: true }); };
Unauthenticated Access (Public Endpoints)
// app/routes/api.public.tsx import { authenticate } from "../shopify.server";
export const loader = async ({ request }: LoaderFunctionArgs) => { const { shop } = await authenticate.public.appProxy(request); // or authenticate.public.checkout(request) return json({ shop }); };
Admin GraphQL API
Basic Patterns
// Simple query
const response = await admin.graphql( query { shop { name email myshopifyDomain plan { displayName } } });
const { data } = await response.json();
// Query with variables
const response = await admin.graphql( query getProduct($id: ID!) { product(id: $id) { id title } }, {
variables: { id: "gid://shopify/Product/123" }
});
// Mutation
const response = await admin.graphql( mutation updateProduct($input: ProductInput!) { productUpdate(input: $input) { product { id title } userErrors { field message code } } }, {
variables: {
input: { id: "gid://shopify/Product/123", title: "New Title" }
}
});
TypeScript Types
// app/types/shopify.ts interface ShopifyProduct { id: string; title: string; handle: string; status: "ACTIVE" | "ARCHIVED" | "DRAFT"; variants: { edges: Array<{ node: ShopifyVariant }> }; }
interface ShopifyVariant { id: string; title: string; price: string; sku: string | null; inventoryQuantity: number; }
interface GraphQLResponse<T> { data: T; errors?: Array<{ message: string }>; extensions?: { cost: QueryCost }; }
interface UserError { field: string[]; message: string; code?: string; }
// Usage const { data } = await response.json() as GraphQLResponse<{ product: ShopifyProduct }>;
Products
Query Products
export const loader = async ({ request }: LoaderFunctionArgs) => { const { admin } = await authenticate.admin(request); const url = new URL(request.url); const cursor = url.searchParams.get("cursor"); const query = url.searchParams.get("query") || "";
const response = await admin.graphql( query getProducts($first: Int!, $after: String, $query: String) { products(first: $first, after: $after, query: $query) { edges { node { id title handle status productType vendor tags totalInventory priceRangeV2 { minVariantPrice { amount currencyCode } maxVariantPrice { amount currencyCode } } featuredImage { url altText } variants(first: 10) { edges { node { id title sku price compareAtPrice inventoryQuantity selectedOptions { name value } } } } metafields(first: 10) { edges { node { namespace key value type } } } } } pageInfo { hasNextPage hasPreviousPage startCursor endCursor } } } , {
variables: { first: 25, after: cursor, query }
});
const { data } = await response.json(); return json({ products: data.products.edges.map((e: any) => e.node), pageInfo: data.products.pageInfo }); };
Query Filters
// Common query filters for products const queries = { // Status active: "status:ACTIVE", draft: "status:DRAFT", archived: "status:ARCHIVED",
// Inventory inStock: "inventory_total:>0", outOfStock: "inventory_total:0", lowStock: "inventory_total:<10",
// Price priceRange: "variants.price:>=10 AND variants.price:<=100",
// Type/Vendor byType: "product_type:Shoes", byVendor: "vendor:Nike",
// Tags byTag: "tag:sale", multipleTags: "tag:sale OR tag:new",
// Date createdAfter: "created_at:>2024-01-01", updatedRecently: "updated_at:>2024-06-01",
// Search titleContains: "title:shirt",
// Combined combined: "status:ACTIVE AND inventory_total:>0 AND tag:featured" };
Create Product
export const action = async ({ request }: ActionFunctionArgs) => { const { admin } = await authenticate.admin(request); const formData = await request.formData();
const response = await admin.graphql( mutation productCreate($input: ProductInput!, $media: [CreateMediaInput!]) { productCreate(input: $input, media: $media) { product { id title handle variants(first: 10) { edges { node { id title price } } } } userErrors { field message code } } } , {
variables: {
input: {
title: formData.get("title"),
descriptionHtml: formData.get("description"),
productType: formData.get("productType"),
vendor: formData.get("vendor"),
tags: (formData.get("tags") as string)?.split(","),
status: "DRAFT",
variants: [{
price: formData.get("price"),
sku: formData.get("sku"),
inventoryQuantities: [{
availableQuantity: parseInt(formData.get("quantity") as string),
locationId: "gid://shopify/Location/123"
}]
}]
},
media: formData.get("imageUrl") ? [{
originalSource: formData.get("imageUrl"),
mediaContentType: "IMAGE"
}] : []
}
});
const { data } = await response.json();
if (data.productCreate.userErrors.length > 0) { return json({ errors: data.productCreate.userErrors }, { status: 400 }); }
return json({ product: data.productCreate.product }); };
Update Product
const response = await admin.graphql( mutation productUpdate($input: ProductInput!) { productUpdate(input: $input) { product { id title updatedAt } userErrors { field message } } }, {
variables: {
input: {
id: "gid://shopify/Product/123",
title: "Updated Title",
descriptionHtml: "<p>New description</p>",
tags: ["new", "sale"],
metafields: [{
namespace: "custom",
key: "color",
value: "red",
type: "single_line_text_field"
}]
}
}
});
Delete Product
const response = await admin.graphql( mutation productDelete($input: ProductDeleteInput!) { productDelete(input: $input) { deletedProductId userErrors { field message } } }, {
variables: {
input: { id: "gid://shopify/Product/123" }
}
});
Update Variant
const response = await admin.graphql( mutation productVariantUpdate($input: ProductVariantInput!) { productVariantUpdate(input: $input) { productVariant { id price sku inventoryQuantity } userErrors { field message } } }, {
variables: {
input: {
id: "gid://shopify/ProductVariant/456",
price: "29.99",
sku: "SKU-001",
compareAtPrice: "39.99"
}
}
});
Orders
Query Orders
const response = await admin.graphql(` query getOrders($first: Int!, $after: String, $query: String) { orders(first: $first, after: $after, query: $query) { edges { node { id name createdAt displayFinancialStatus displayFulfillmentStatus email phone
customer {
id
email
firstName
lastName
}
shippingAddress {
address1
address2
city
province
country
zip
}
totalPriceSet {
shopMoney { amount currencyCode }
}
subtotalPriceSet {
shopMoney { amount currencyCode }
}
totalShippingPriceSet {
shopMoney { amount currencyCode }
}
totalTaxSet {
shopMoney { amount currencyCode }
}
lineItems(first: 50) {
edges {
node {
id
title
quantity
sku
variant { id title }
originalUnitPriceSet {
shopMoney { amount currencyCode }
}
discountedUnitPriceSet {
shopMoney { amount currencyCode }
}
}
}
}
transactions(first: 10) {
id
kind
status
amountSet {
shopMoney { amount currencyCode }
}
}
fulfillments {
id
status
trackingInfo { number url company }
}
}
}
pageInfo { hasNextPage endCursor }
}
} `, { variables: { first: 25, after: cursor, query: "fulfillment_status:unfulfilled AND financial_status:paid" } });
Order Query Filters
const orderQueries = { // Financial status paid: "financial_status:paid", pending: "financial_status:pending", refunded: "financial_status:refunded",
// Fulfillment status unfulfilled: "fulfillment_status:unfulfilled", fulfilled: "fulfillment_status:fulfilled", partial: "fulfillment_status:partial",
// Date ranges today: "created_at:today", thisWeek: "created_at:past_week", thisMonth: "created_at:past_month", dateRange: "created_at:>=2024-01-01 AND created_at:<=2024-12-31",
// Customer byEmail: "email:customer@example.com",
// Tags byTag: "tag:wholesale",
// Risk highRisk: "risk_level:high",
// Combined readyToShip: "fulfillment_status:unfulfilled AND financial_status:paid" };
Create Draft Order
const response = await admin.graphql( mutation draftOrderCreate($input: DraftOrderInput!) { draftOrderCreate(input: $input) { draftOrder { id invoiceUrl totalPrice } userErrors { field message } } }, {
variables: {
input: {
email: "customer@example.com",
lineItems: [{
variantId: "gid://shopify/ProductVariant/123",
quantity: 2
}],
shippingAddress: {
firstName: "John",
lastName: "Doe",
address1: "123 Main St",
city: "New York",
province: "NY",
country: "US",
zip: "10001"
},
appliedDiscount: {
value: 10,
valueType: "PERCENTAGE",
title: "10% Off"
},
note: "Special instructions"
}
}
});
Complete Draft Order
const response = await admin.graphql( mutation draftOrderComplete($id: ID!) { draftOrderComplete(id: $id) { draftOrder { id order { id name } } userErrors { field message } } }, {
variables: { id: "gid://shopify/DraftOrder/123" }
});
Mark Order as Paid
const response = await admin.graphql( mutation orderMarkAsPaid($input: OrderMarkAsPaidInput!) { orderMarkAsPaid(input: $input) { order { id displayFinancialStatus } userErrors { field message } } }, {
variables: {
input: { id: "gid://shopify/Order/123" }
}
});
Cancel Order
const response = await admin.graphql( mutation orderCancel($orderId: ID!, $reason: OrderCancelReason!, $refund: Boolean!, $restock: Boolean!) { orderCancel(orderId: $orderId, reason: $reason, refund: $refund, restock: $restock) { orderCancelUserErrors { field message code } job { id } } }, {
variables: {
orderId: "gid://shopify/Order/123",
reason: "CUSTOMER",
refund: true,
restock: true
}
});
Customers
Query Customers
const response = await admin.graphql(` query getCustomers($first: Int!, $query: String) { customers(first: $first, query: $query) { edges { node { id email firstName lastName phone createdAt
numberOfOrders
amountSpent { amount currencyCode }
defaultAddress {
address1
city
province
country
zip
}
addresses(first: 5) {
address1
city
country
}
tags
note
metafields(first: 10, namespace: "custom") {
edges {
node { key value type }
}
}
emailMarketingConsent {
marketingState
consentUpdatedAt
}
}
}
pageInfo { hasNextPage endCursor }
}
} `, { variables: { first: 25, query: "email:*@gmail.com" } });
Create Customer
const response = await admin.graphql( mutation customerCreate($input: CustomerInput!) { customerCreate(input: $input) { customer { id email } userErrors { field message } } }, {
variables: {
input: {
email: "new@customer.com",
firstName: "John",
lastName: "Doe",
phone: "+1234567890",
addresses: [{
address1: "123 Main St",
city: "New York",
province: "NY",
country: "US",
zip: "10001"
}],
tags: ["vip", "wholesale"],
metafields: [{
namespace: "custom",
key: "loyalty_points",
value: "100",
type: "number_integer"
}],
emailMarketingConsent: {
marketingState: "SUBSCRIBED",
marketingOptInLevel: "SINGLE_OPT_IN"
}
}
}
});
Update Customer
const response = await admin.graphql( mutation customerUpdate($input: CustomerInput!) { customerUpdate(input: $input) { customer { id email tags } userErrors { field message } } }, {
variables: {
input: {
id: "gid://shopify/Customer/123",
tags: ["vip", "wholesale", "loyalty"],
note: "Important customer"
}
}
});
Collections
Query Collections
const response = await admin.graphql(` query getCollections($first: Int!) { collections(first: $first) { edges { node { id title handle descriptionHtml productsCount sortOrder
image {
url
altText
}
ruleSet {
appliedDisjunctively
rules {
column
relation
condition
}
}
}
}
}
} `, { variables: { first: 50 } });
Create Collection
// Manual collection
const response = await admin.graphql( mutation collectionCreate($input: CollectionInput!) { collectionCreate(input: $input) { collection { id title handle } userErrors { field message } } }, {
variables: {
input: {
title: "Summer Sale",
descriptionHtml: "<p>Hot summer deals!</p>",
products: [
"gid://shopify/Product/123",
"gid://shopify/Product/456"
]
}
}
});
// Smart collection (automated)
const response = await admin.graphql( mutation collectionCreate($input: CollectionInput!) { collectionCreate(input: $input) { collection { id title } userErrors { field message } } }, {
variables: {
input: {
title: "Sale Items",
ruleSet: {
appliedDisjunctively: false,
rules: [
{ column: "TAG", relation: "EQUALS", condition: "sale" },
{ column: "VARIANT_COMPARE_AT_PRICE", relation: "IS_SET", condition: "" }
]
}
}
}
});
Add Products to Collection
const response = await admin.graphql( mutation collectionAddProducts($id: ID!, $productIds: [ID!]!) { collectionAddProducts(id: $id, productIds: $productIds) { collection { id productsCount } userErrors { field message } } }, {
variables: {
id: "gid://shopify/Collection/123",
productIds: [
"gid://shopify/Product/456",
"gid://shopify/Product/789"
]
}
});
Inventory
Query Inventory Levels
const response = await admin.graphql( query getInventoryLevels($inventoryItemId: ID!) { inventoryItem(id: $inventoryItemId) { id sku tracked inventoryLevels(first: 10) { edges { node { id available location { id name } } } } } }, {
variables: { inventoryItemId: "gid://shopify/InventoryItem/123" }
});
Adjust Inventory
const response = await admin.graphql( mutation inventoryAdjustQuantities($input: InventoryAdjustQuantitiesInput!) { inventoryAdjustQuantities(input: $input) { inventoryAdjustmentGroup { reason changes { name delta } } userErrors { field message } } }, {
variables: {
input: {
reason: "correction",
name: "available",
changes: [{
inventoryItemId: "gid://shopify/InventoryItem/123",
locationId: "gid://shopify/Location/456",
delta: 10 // positive = add, negative = subtract
}]
}
}
});
Set Inventory Level
const response = await admin.graphql( mutation inventorySetQuantities($input: InventorySetQuantitiesInput!) { inventorySetQuantities(input: $input) { inventoryAdjustmentGroup { changes { name delta } } userErrors { field message } } }, {
variables: {
input: {
reason: "correction",
name: "available",
quantities: [{
inventoryItemId: "gid://shopify/InventoryItem/123",
locationId: "gid://shopify/Location/456",
quantity: 100 // set absolute quantity
}]
}
}
});
Get Locations
const response = await admin.graphql( query getLocations { locations(first: 10) { edges { node { id name address { address1 city country } isActive fulfillsOnlineOrders } } } });
Fulfillments
Create Fulfillment
const response = await admin.graphql( mutation fulfillmentCreateV2($fulfillment: FulfillmentV2Input!) { fulfillmentCreateV2(fulfillment: $fulfillment) { fulfillment { id status trackingInfo { number url company } } userErrors { field message } } }, {
variables: {
fulfillment: {
lineItemsByFulfillmentOrder: [{
fulfillmentOrderId: "gid://shopify/FulfillmentOrder/123",
fulfillmentOrderLineItems: [{
id: "gid://shopify/FulfillmentOrderLineItem/456",
quantity: 1
}]
}],
trackingInfo: {
number: "1Z999AA10123456784",
url: "https://tracking.example.com/1Z999AA10123456784",
company: "UPS"
},
notifyCustomer: true
}
}
});
Get Fulfillment Orders
const response = await admin.graphql( query getFulfillmentOrders($orderId: ID!) { order(id: $orderId) { fulfillmentOrders(first: 10) { edges { node { id status assignedLocation { name } lineItems(first: 50) { edges { node { id totalQuantity remainingQuantity lineItem { title sku } } } } } } } } }, {
variables: { orderId: "gid://shopify/Order/123" }
});
Update Tracking
const response = await admin.graphql( mutation fulfillmentTrackingInfoUpdateV2($fulfillmentId: ID!, $trackingInfoInput: FulfillmentTrackingInput!) { fulfillmentTrackingInfoUpdateV2(fulfillmentId: $fulfillmentId, trackingInfoInput: $trackingInfoInput) { fulfillment { id trackingInfo { number url } } userErrors { field message } } }, {
variables: {
fulfillmentId: "gid://shopify/Fulfillment/123",
trackingInfoInput: {
number: "NEW123456",
url: "https://tracking.example.com/NEW123456",
company: "FedEx"
}
}
});
Discounts
Create Automatic Discount
const response = await admin.graphql( mutation discountAutomaticBasicCreate($automaticBasicDiscount: DiscountAutomaticBasicInput!) { discountAutomaticBasicCreate(automaticBasicDiscount: $automaticBasicDiscount) { automaticDiscountNode { id automaticDiscount { ... on DiscountAutomaticBasic { title startsAt endsAt } } } userErrors { field message } } }, {
variables: {
automaticBasicDiscount: {
title: "Summer Sale 20% Off",
startsAt: "2024-06-01T00:00:00Z",
endsAt: "2024-08-31T23:59:59Z",
minimumRequirement: {
subtotal: { greaterThanOrEqualToSubtotal: "50.00" }
},
customerGets: {
value: { percentage: 0.20 },
items: { all: true }
}
}
}
});
Create Discount Code
const response = await admin.graphql( mutation discountCodeBasicCreate($basicCodeDiscount: DiscountCodeBasicInput!) { discountCodeBasicCreate(basicCodeDiscount: $basicCodeDiscount) { codeDiscountNode { id codeDiscount { ... on DiscountCodeBasic { title codes(first: 1) { edges { node { code } } } } } } userErrors { field message } } }, {
variables: {
basicCodeDiscount: {
title: "Welcome Discount",
code: "WELCOME10",
startsAt: "2024-01-01T00:00:00Z",
usageLimit: 1000,
appliesOncePerCustomer: true,
customerGets: {
value: { percentage: 0.10 },
items: { all: true }
},
customerSelection: { all: true }
}
}
});
Metafields
Get Metafields
// On a product
const response = await admin.graphql( query getProductMetafields($id: ID!) { product(id: $id) { metafields(first: 50) { edges { node { id namespace key value type description } } } } }, { variables: { id: "gid://shopify/Product/123" } });
// By specific namespace/key
const response = await admin.graphql( query getMetafield($ownerId: ID!, $namespace: String!, $key: String!) { product(id: $ownerId) { metafield(namespace: $namespace, key: $key) { id value type } } }, {
variables: {
ownerId: "gid://shopify/Product/123",
namespace: "custom",
key: "specifications"
}
});
Set Metafields
const response = await admin.graphql( mutation metafieldsSet($metafields: [MetafieldsSetInput!]!) { metafieldsSet(metafields: $metafields) { metafields { id namespace key value } userErrors { field message } } }, {
variables: {
metafields: [
{
ownerId: "gid://shopify/Product/123",
namespace: "custom",
key: "material",
value: "Cotton",
type: "single_line_text_field"
},
{
ownerId: "gid://shopify/Product/123",
namespace: "custom",
key: "care_instructions",
value: JSON.stringify(["Machine wash cold", "Tumble dry low"]),
type: "list.single_line_text_field"
},
{
ownerId: "gid://shopify/Product/123",
namespace: "custom",
key: "is_featured",
value: "true",
type: "boolean"
},
{
ownerId: "gid://shopify/Product/123",
namespace: "custom",
key: "rating",
value: "4.5",
type: "number_decimal"
}
]
}
});
Metafield Types Reference
Type Example Value
single_line_text_field
"Hello"
multi_line_text_field
"Line 1\nLine 2"
number_integer
"42"
number_decimal
"3.14"
boolean
"true" or "false"
date
"2024-01-15"
date_time
"2024-01-15T10:30:00Z"
json
"{"key":"value"}"
url
color
"#FF0000"
list.single_line_text_field
"["item1","item2"]"
product_reference
"gid://shopify/Product/123"
file_reference
"gid://shopify/MediaImage/123"
Delete Metafield
const response = await admin.graphql( mutation metafieldDelete($input: MetafieldDeleteInput!) { metafieldDelete(input: $input) { deletedId userErrors { field message } } }, {
variables: {
input: { id: "gid://shopify/Metafield/123" }
}
});
Files & Media
Upload File (Staged Upload)
// Step 1: Create staged upload
const stageResponse = await admin.graphql( mutation stagedUploadsCreate($input: [StagedUploadInput!]!) { stagedUploadsCreate(input: $input) { stagedTargets { url resourceUrl parameters { name value } } userErrors { field message } } }, {
variables: {
input: [{
resource: "IMAGE",
filename: "product-image.jpg",
mimeType: "image/jpeg",
fileSize: "1024000",
httpMethod: "POST"
}]
}
});
// Step 2: Upload file to staged URL (using fetch) const { stagedTargets } = stageResponse.stagedUploadsCreate; const target = stagedTargets[0];
const formData = new FormData(); target.parameters.forEach((param: any) => { formData.append(param.name, param.value); }); formData.append("file", fileBlob);
await fetch(target.url, { method: "POST", body: formData });
// Step 3: Create file in Shopify
const fileResponse = await admin.graphql( mutation fileCreate($files: [FileCreateInput!]!) { fileCreate(files: $files) { files { ... on MediaImage { id image { url } } } userErrors { field message } } }, {
variables: {
files: [{
originalSource: target.resourceUrl,
contentType: "IMAGE"
}]
}
});
Add Media to Product
const response = await admin.graphql( mutation productCreateMedia($productId: ID!, $media: [CreateMediaInput!]!) { productCreateMedia(productId: $productId, media: $media) { media { ... on MediaImage { id image { url altText } } } mediaUserErrors { field message } } }, {
variables: {
productId: "gid://shopify/Product/123",
media: [{
originalSource: "https://example.com/image.jpg",
mediaContentType: "IMAGE",
alt: "Product image description"
}]
}
});
Bulk Operations
Run Bulk Query
// Start bulk operation
const response = await admin.graphql( mutation bulkOperationRunQuery($query: String!) { bulkOperationRunQuery(query: $query) { bulkOperation { id status } userErrors { field message } } }, {
variables: {
query: { products { edges { node { id title handle variants { edges { node { id sku price inventoryQuantity } } } } } } }
}
});
// Poll for completion
const pollBulkOperation = async (admin: any): Promise<string | null> => {
const response = await admin.graphql( query { currentBulkOperation { id status url errorCode objectCount } } );
const { data } = await response.json(); const op = data.currentBulkOperation;
if (op.status === "COMPLETED") {
return op.url; // JSONL file URL
} else if (op.status === "FAILED") {
throw new Error(Bulk operation failed: ${op.errorCode});
}
// Still running, poll again await new Promise(resolve => setTimeout(resolve, 2000)); return pollBulkOperation(admin); };
// Download and process results const resultsUrl = await pollBulkOperation(admin); const resultsResponse = await fetch(resultsUrl); const jsonlText = await resultsResponse.text(); const results = jsonlText.split("\n").filter(Boolean).map(JSON.parse);
Bulk Mutation
// Create staged upload for JSONL input
const stageResponse = await admin.graphql( mutation stagedUploadsCreate($input: [StagedUploadInput!]!) { stagedUploadsCreate(input: $input) { stagedTargets { url resourceUrl parameters { name value } } userErrors { field message } } }, {
variables: {
input: [{
resource: "BULK_MUTATION_VARIABLES",
filename: "bulk-input.jsonl",
mimeType: "text/jsonl",
httpMethod: "POST"
}]
}
});
// Upload JSONL file with mutations const jsonlContent = products.map(p => JSON.stringify({ input: { id: p.id, title: p.title } })).join("\n");
// ... upload to staged URL ...
// Run bulk mutation
const bulkResponse = await admin.graphql( mutation bulkOperationRunMutation($mutation: String!, $stagedUploadPath: String!) { bulkOperationRunMutation(mutation: $mutation, stagedUploadPath: $stagedUploadPath) { bulkOperation { id status } userErrors { field message } } }, {
variables: {
mutation: mutation productUpdate($input: ProductInput!) { productUpdate(input: $input) { product { id } userErrors { field message } } } ,
stagedUploadPath: stagedTarget.resourceUrl
}
});
Pagination Utility
// app/utils/shopify-pagination.server.ts export interface PageInfo { hasNextPage: boolean; hasPreviousPage: boolean; startCursor: string; endCursor: string; }
export async function fetchAllPages<T>( admin: any, query: string, variables: Record<string, any>, connectionPath: string[], options?: { maxPages?: number } ): Promise<T[]> { const results: T[] = []; let hasNextPage = true; let cursor: string | null = null; let pageCount = 0; const maxPages = options?.maxPages ?? Infinity;
while (hasNextPage && pageCount < maxPages) { const response = await admin.graphql(query, { variables: { ...variables, after: cursor } });
const { data, errors } = await response.json();
if (errors) {
throw new Error(`GraphQL error: ${errors[0].message}`);
}
// Navigate to connection
let connection = data;
for (const key of connectionPath) {
connection = connection[key];
}
results.push(...connection.edges.map((edge: any) => edge.node));
hasNextPage = connection.pageInfo.hasNextPage;
cursor = connection.pageInfo.endCursor;
pageCount++;
}
return results; }
// Usage example
const allProducts = await fetchAllPages(
admin,
query($first: Int!, $after: String) { products(first: $first, after: $after) { edges { node { id title status } } pageInfo { hasNextPage endCursor } } },
{ first: 250 },
["products"]
);
Admin REST API
// GET request const response = await admin.rest.get({ path: "products", query: { limit: 50, status: "active" } }); const products = response.body.products;
// GET single resource
const response = await admin.rest.get({
path: products/${productId}
});
// POST request const response = await admin.rest.post({ path: "products", data: { product: { title: "New Product", body_html: "<p>Description</p>", vendor: "Vendor Name" } } });
// PUT request
const response = await admin.rest.put({
path: products/${productId},
data: {
product: { title: "Updated Title" }
}
});
// DELETE request
const response = await admin.rest.delete({
path: products/${productId}
});
Storefront API
Setup
// app/utils/storefront.server.ts
export async function storefrontQuery(shop: string, query: string, variables?: any) {
const response = await fetch(
https://${shop}/api/2025-01/graphql.json,
{
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Shopify-Storefront-Access-Token": process.env.STOREFRONT_ACCESS_TOKEN!
},
body: JSON.stringify({ query, variables })
}
);
return response.json();
}
Query Products (Storefront)
const { data } = await storefrontQuery(shop, query getProducts($first: Int!) { products(first: $first) { edges { node { id title handle description priceRange { minVariantPrice { amount currencyCode } } images(first: 1) { edges { node { url altText } } } variants(first: 10) { edges { node { id title price { amount currencyCode } availableForSale } } } } } } }, { first: 20 });
Cart Operations (Storefront)
// Create cart
const { data } = await storefrontQuery(shop, mutation cartCreate($input: CartInput!) { cartCreate(input: $input) { cart { id checkoutUrl lines(first: 10) { edges { node { id quantity merchandise { ... on ProductVariant { id title } } } } } } userErrors { field message } } }, {
input: {
lines: [{
merchandiseId: "gid://shopify/ProductVariant/123",
quantity: 1
}]
}
});
// Add to cart
const { data } = await storefrontQuery(shop, mutation cartLinesAdd($cartId: ID!, $lines: [CartLineInput!]!) { cartLinesAdd(cartId: $cartId, lines: $lines) { cart { id } userErrors { field message } } }, {
cartId: "gid://shopify/Cart/abc123",
lines: [{ merchandiseId: "gid://shopify/ProductVariant/456", quantity: 2 }]
});
Error Handling
// app/utils/shopify-errors.server.ts export interface ShopifyUserError { field: string[]; message: string; code?: string; }
export class ShopifyAPIError extends Error { constructor( message: string, public userErrors?: ShopifyUserError[], public graphqlErrors?: any[] ) { super(message); this.name = "ShopifyAPIError"; } }
export async function executeGraphQL<T>( admin: any, query: string, variables?: Record<string, any>, mutationPath?: string ): Promise<T> { const response = await admin.graphql(query, { variables }); const { data, errors, extensions } = await response.json();
// GraphQL-level errors (syntax, validation)
if (errors?.length > 0) {
throw new ShopifyAPIError(
GraphQL Error: ${errors[0].message},
undefined,
errors
);
}
// User errors (business logic)
if (mutationPath) {
const mutationResult = data[mutationPath];
if (mutationResult?.userErrors?.length > 0) {
throw new ShopifyAPIError(
Mutation Error: ${mutationResult.userErrors[0].message},
mutationResult.userErrors
);
}
}
// Log rate limit info
if (extensions?.cost) {
const { requestedQueryCost, throttleStatus } = extensions.cost;
if (throttleStatus.currentlyAvailable < 100) {
console.warn(Low rate limit: ${throttleStatus.currentlyAvailable} remaining);
}
}
return data; }
// Usage
try {
const data = await executeGraphQL(
admin,
mutation productUpdate($input: ProductInput!) { productUpdate(input: $input) { product { id } userErrors { field message code } } },
{ input: { id: productId, title: newTitle } },
"productUpdate"
);
} catch (error) {
if (error instanceof ShopifyAPIError) {
if (error.userErrors) {
return json({ errors: error.userErrors }, { status: 422 });
}
}
throw error;
}
Rate Limiting
// Check rate limit status
const response = await admin.graphql(...);
const { extensions } = await response.json();
if (extensions?.cost) { const { requestedQueryCost, actualQueryCost, throttleStatus } = extensions.cost; console.log({ requested: requestedQueryCost, actual: actualQueryCost, available: throttleStatus.currentlyAvailable, maximum: throttleStatus.maximumAvailable, restoreRate: throttleStatus.restoreRate }); }
// Exponential backoff for rate limits export async function withRetry<T>( fn: () => Promise<T>, maxRetries = 3, baseDelay = 1000 ): Promise<T> { let lastError: Error | undefined;
for (let attempt = 0; attempt < maxRetries; attempt++) { try { return await fn(); } catch (error: any) { lastError = error;
// Check if rate limited (429)
if (error?.response?.status === 429 || error?.message?.includes("Throttled")) {
const delay = baseDelay * Math.pow(2, attempt);
console.log(`Rate limited, retrying in ${delay}ms...`);
await new Promise(resolve => setTimeout(resolve, delay));
continue;
}
throw error;
}
}
throw lastError; }
Rate Limits:
-
GraphQL: 1000 points, refill 50/sec (Standard), higher for Plus
-
REST: 40 requests, refill 2/sec (Standard)
-
Use first: 250 max for pagination
-
Bulk operations for >10k items