shopify-api
SKILL.md
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 |
"https://example.com" |
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: 250max for pagination - Bulk operations for >10k items
Weekly Installs
10
Repository
toilahuongg/sho…ents-kitGitHub Stars
6
First Seen
Jan 30, 2026
Security Audits
Installed on
claude-code9
github-copilot9
codex9
gemini-cli8
amp8
kimi-cli8