A
app-shopify-admin-graphql
About app-shopify-admin-graphql
A skill for developers building Shopify Admin API integrations with GraphQL. It covers authenticated requests, pagination, throttling, and error handling across request-based and background contexts.
Key features
- 01Use Remix admin authentication
- 02Query and mutate Shopify data
- 03Paginate large result sets
- 04Handle throttling with retries
- 05Process GraphQL and HTTP errors
Use cases
- 01Fetch shop, customer, order, or product data
- 02Update Shopify records through GraphQL
- 03Run API calls in webhook jobs
Install
Copy the command and run it in a terminal.
$
npx skills add https:/ / github.com/ niccos-shopify-workspace/ shopify-cursor-skills --skill app-shopify-admin-graphqlAfter installing, type/app-shopify-admin-graphqlin Claude Code to confirm it loaded.
Details
From the project READMEShopify Admin GraphQL
Use this skill when adding or changing code that talks to the Shopify Admin API via GraphQL.
When to Use
- Querying Shopify data (shop, customers, orders, products, inventory)
- Mutating Shopify data (creating/updating customers, orders, products)
- Implementing pagination for large datasets
- Handling API throttling and rate limits
- Working with metafields or other Shopify resources
Getting the GraphQL Client
In Remix Loaders/Actions (Recommended)
Use authenticate.admin() from @shopify/shopify-app-remix:
import { authenticate } from "../shopify.server";
export const loader = async ({ request }: LoaderFunctionArgs) => {
const { admin } = await authenticate.admin(request);
const response = await admin.graphql(`
query GetShopDetails {
shop {
id
name
myshopifyDomain
plan {
displayName
}
}
}
`);
const { data } = await response.json();
return json({ shop: data.shop });
};
With Variables
export const action = async ({ request }: ActionFunctionArgs) => {
const { admin } = await authenticate.admin(request);
const formData = await request.formData();
const email = formData.get("email") as string;
// Validate email format to prevent query manipulation
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!email || !emailRegex.test(email)) {
return json({ error: "Invalid email format" }, { status: 400 });
}
// Escape quotes and wrap in quotes to treat as literal value
const sanitizedEmail = email.replace(/"/g, '\\"');
const response = await admin.graphql(`
query FindCustomerByEmail($query: String!) {
customers(first: 1, query: $query) {
edges {
node {
id
email
phone
firstName
lastName
}
}
}
}
`, {
variables: { query: `email:"${sanitizedEmail}"` }
});
const { data } = await response.json();
return json({ customer: data.customers.edges[0]?.node });
};
Background Jobs / Webhooks (Offline Access)
When you don't have a request context, use offline session tokens:
import { unauthenticated } from "../shopify.server";
export async function processWebhook(shop: string) {
const { admin } = await unauthenticated.admin(shop);
const response = await admin.graphql(`
query GetShop {
shop {
name
}
}
`);
const { data } = await response.json();
return data.shop;
}
Common Query Patterns
Shop Details
query GetShopDetails {
shop {
id
name
email
myshopifyDomain
primaryDomain {
url
}
plan {
displayName
}
currencyCode
timezoneAbbreviation
}
}
Products with Pagination
query GetProducts($first: Int!, $after: String) {
products(first: $first, after: $after) {
pageInfo {
hasNextPage
endCursor
}
edges {
node {
id
title
handle
status
variants(first: 10) {
edges {
node {
id
title
price
sku
}
}
}
}
}
}
}
Customer Lookup
query FindCustomer($query: String!) {
customers(first: 1, query: $query) {
edges {
node {
id
email
phone
firstName
lastName
ordersCount
totalSpent
}
}
}
}
Order by ID
query GetOrder($id: ID!) {
order(id: $id) {
id
name
email
phone
totalPriceSet {
shopMoney {
amount
currencyCode
}
}
lineItems(first: 50) {
edges {
node {
title
quantity
variant {
id
sku
}
}
}
}
shippingAddress {
address1
city
country
}
}
}
Handling Throttling
Shopify uses a leaky bucket algorithm for rate limiting. For bulk operations or background jobs, implement retry logic:
interface RetryOptions {
maxRetries?: number;
initialDelay?: number;
maxDelay?: number;
}
async function executeWithRetry<T>(
admin: AdminApiContext,
query: string,
variables?: Record<string, unknown>,
options: RetryOptions = {}
): Promise<T> {
const { maxRetries = 3, initialDelay = 1000, maxDelay = 10000 } = options;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
const response = await admin.graphql(query, { variables });
const result = await response.json();
if (result.errors?.some((e: any) => e.extensions?.code === "THROTTLED")) {
throw new Error("THROTTLED");
}
return result.data as T;
} catch (error) {
if (attempt === maxRetries) throw error;
const delay = Math.min(initialDelay * Math.pow(2, attempt), maxDelay);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
throw new Error("Max retries exceeded");
}
Error Handling
const response = await admin.graphql(query, { variables });
const { data, errors } = await response.json();
if (errors) {
// Handle GraphQL errors
console.error("GraphQL errors:", errors);
// Check for specific error types
const throttled = errors.some((e: any) =>
e.extensions?.code === "THROTTLED"
);
const notFound = errors.some((e: any) =>
e.message?.includes("not found")
);
if (throttled) {
// Retry with backoff
}
}
Best Practices
- Use operation names for debugging:
query GetShopDetails { ... } - Request only needed fields to reduce response size and improve performance
- Use pagination for lists - never request unbounded data
- Handle errors gracefully - check for both
errorsarray and HTTP errors - Implement retries with exponential backoff for background jobs
- Use fragments for repeated field selections across queries
- Prefer GraphQL over REST for complex queries with relationships
References
Comments
Markdown supported