shopify-client 1.0.0

Type-safe, async Rust client for the Shopify Admin and Storefront APIs
Documentation

shopify-client

Type-safe, async Rust client for the Shopify Admin and Storefront APIs

Crates.io Documentation License: MIT Shopify API

InstallationAdmin APIStorefront APIBulk OperationsDocs


// Admin API — orders, subscriptions, bulk exports, webhooks, …
let admin = ShopifyClient::new(shop_url.clone(), admin_token, None);
let order = admin.order.get_with_id(&id).await?;

// Storefront API — products, cart, customer, search, … (behind the `storefront` feature)
let store = ShopifyStorefront::new(shop_url, storefront_token, None);
let product = store.product.get_by_handle("my-product").await?;

Highlights

Two clients, one crate ShopifyClient for the Admin API, ShopifyStorefront for the Storefront API — each with its own token, endpoint, and surface. Storefront lives behind a Cargo feature so admin-only apps pay nothing for it.
9 admin service modules Orders, Subscriptions, Discounts, Cart Transforms, App Installation, Shopify Functions, Shop, Storefront Tokens, Bulk Operations
8 storefront service modules Products, Collections, Cart, Customer, Search, Content (pages/blogs/articles/menus), Shop, Metaobjects
Bulk operations Prebuilt export templates for products, orders, collections, customers, inventory, and draft orders with typed JSONL parsing
Typed everything Strongly typed requests, responses, filters, JSONL export lines, GraphQL inputs — no raw strings
Async / await Built on reqwest with tokio — non-blocking by default, one shared connection pool
Webhook parsing HMAC-verified parsing for customer data, customer redact, and shop redact compliance webhooks
Request callbacks Optional before/after hooks for logging, metrics, and observability

Installation

shopify-client is a single crate. The Admin API is always available; the Storefront API is gated behind the storefront Cargo feature.

[dependencies]

# Admin API only (the common case)
shopify-client = "0.19"

# Admin API + Storefront API
shopify-client = { version = "0.19", features = ["storefront"] }

With the storefront feature enabled, the Storefront client is available as shopify_client::storefront:

use shopify_client::ShopifyClient;             // Admin
use shopify_client::storefront::ShopifyStorefront; // Storefront (feature-gated)

The two clients are fully independent — different access tokens, different endpoints, different rate limits. Construct whichever you need; apps that need both construct one of each.


Admin API

The Admin API is what apps use to manage merchant data: orders, products, subscriptions, discounts, app metafields, bulk exports, and more. Requests use an Admin access token sent as the X-Shopify-Access-Token header, against the /admin/api/{version}/graphql.json (and per-resource REST) endpoints.

Getting Started

[dependencies]
shopify-client = "0.19"
use shopify_client::ShopifyClient;

#[tokio::main]
async fn main() {
    let client = ShopifyClient::new(
        "https://your-shop.myshopify.com".to_string(),
        "your-admin-access-token".to_string(),
        None, // defaults to API version 2026-01
    );

    // Every service is a field on the client
    let order = client.order.get_with_id(&"1234567890".to_string()).await;
    let shop  = client.shop.get().await;
}

API Coverage

Service Protocol Operations
client.order REST get_with_id, get_with_name, patch
client.subscription GraphQL create_recurring, create_usage, create_combined, cancel, extend_trial, update_capped_amount, create_usage_record, get_active_subscriptions
client.discount GraphQL create_automatic_app_discount, update_automatic_app_discount, get_discount_nodes
client.app_installation GraphQL get_current, set_metafields, get_metafield, list_metafields
client.cart_transform GraphQL create, set_metafields
client.shopify_functions GraphQL list
client.shop GraphQL get, get_status
client.storefront_access_token GraphQL list, create, delete
client.bulk_operation GraphQL run_query, run_mutation, cancel, get, list, create_staged_upload, export_*, stream_*

Admin services live under shopify_client::admin::* (also re-exported as shopify_client::services::* for back-compat with pre-0.19 releases). Request/response types live under shopify_client::types::*.

Examples

Orders

// Get by ID
let resp = client.order.get_with_id(&order_id).await?;
println!("Order: {} - {}", resp.order.name, resp.order.email);

// Get by name
let resp = client.order.get_with_name(&"1001".to_string()).await?;

// Update tags
let patch = PatchOrderRequest {
    order: PatchOrder {
        tags: vec!["processed".into(), "priority".into()],
    },
};
client.order.patch(&order_id, &patch).await?;

Subscriptions

use shopify_client::types::subscription::*;

let request = CreateRecurringSubscriptionRequest {
    name: "Premium Plan".to_string(),
    price: 29.99,
    currency_code: "USD".to_string(),
    return_url: "https://your-app.com/billing".to_string(),
    interval: Some(AppPricingInterval::Every30Days),
    trial_days: Some(7),
    test: Some(true),
    discount: None,
};

let resp = client.subscription.create_recurring(&request).await?;
println!("Confirm at: {}", resp.confirmation_url);

Shop Info

let resp = client.shop.get().await?;
println!("{} ({})", resp.shop.name, resp.shop.plan.public_display_name);
println!("Owner: {}", resp.shop.account_owner.email);

// Lightweight status check — useful for health checks / setup wizards
let status = client.shop.get_status().await?;
println!("Setup required: {}", status.shop.setup_required);

Create automatic discount

use shopify_client::types::discount::DiscountAutomaticAppInput;

let input = DiscountAutomaticAppInput {
    title: "Summer Sale".to_string(),
    function_handle: "my-discount-function".to_string(),
    starts_at: "2024-06-01T00:00:00Z".to_string(),
    ends_at: None,
    combines_with: None,
    discount_classes: None,
    context: None,
    metafields: None,
    applies_on_subscription: None,
    applies_on_one_time_purchase: None,
    recurring_cycle_limit: None,
};
client.discount.create_automatic_app_discount(&input).await?;

Create cart transform

use shopify_client::types::cart_transform::{CartTransformCreateInput, MetafieldInput};

let input = CartTransformCreateInput::new()
    .with_function_handle("my-cart-transform".to_string())
    .with_block_on_failure(false)
    .with_metafields(vec![
        MetafieldInput::new(
            "$app".to_string(), "config".to_string(),
            r#"{"bundleDiscount": 10}"#.to_string(), "json".to_string(),
        ),
    ]);
client.cart_transform.create(&input).await?;

Update metafields with CAS

use shopify_client::types::cart_transform::MetafieldsSetInput;

let metafields = vec![
    MetafieldsSetInput::new(
        owner_id.clone(), "$app".to_string(), "config".to_string(),
        r#"{"bundleDiscount": 20}"#.to_string(), "json".to_string(),
    ).with_compare_digest(Some("fd6b737...".to_string())),
];
client.cart_transform.set_metafields(&metafields).await?;

Manage storefront access tokens

The Admin API issues the Storefront access tokens used by the Storefront client below.

// Create a public access token for a custom storefront / SDK
let resp = client.storefront_access_token
    .create(&"My Custom Storefront".to_string())
    .await?;
println!("Token: {}", resp.storefront_access_token.access_token);

// List all existing tokens
let tokens = client.storefront_access_token.list().await?;

// Revoke when no longer needed
client.storefront_access_token.delete(&token_id).await?;

Bulk Operations

The bulk operations API lets you export or import millions of objects asynchronously. This client provides two layers:

Layer For Example
Prebuilt templates Common exports with zero GraphQL client.bulk_operation.export_products(params)
Raw operations Custom queries / mutations client.bulk_operation.run_query(graphql, None)

Export Templates

Six prebuilt exports, each with typed filter params and comprehensive field coverage:

Template Resource Children Filter Struct
export_products Products Variants, Media ProductQueryParams
export_orders Orders LineItems OrderQueryParams
export_collections Collections Products CollectionQueryParams
export_customers Customers Addresses CustomerQueryParams
export_inventory_items InventoryItems Levels InventoryItemQueryParams
export_draft_orders DraftOrders LineItems DraftOrderQueryParams

Typed Filters

Build search filters with type safety — no raw query strings:

use shopify_client::types::order::*;
use shopify_client::common::query_filter::DateFilter;

let params = OrderQueryParams {
    status: Some(OrderStatus::Open),
    financial_status: Some(OrderFinancialStatus::Paid),
    created_at: Some(DateFilter::OnOrAfter("2025-01-01".to_string())),
    ..Default::default()
};

client.bulk_operation.export_orders(Some(&params)).await?;

Available filter primitives:

Filter Variants
DateFilter Exact, Before, After, OnOrBefore, OnOrAfter, Range
NumericFilter<T> Exact, GreaterThan, LessThan, GreaterOrEqual, LessOrEqual

Full Export Workflow

Bulk exports are a three-step process: start the export, poll until Shopify finishes, then consume the result. The JSONL result URL points to a temporary file hosted by Shopify that can be multi-GB for large shops (millions of products/orders). The stream_* methods download and parse this file in chunks so your app uses constant memory regardless of file size.

use shopify_client::ShopifyClient;
use shopify_client::types::bulk_operation::{BulkOperationStatus, ProductExportLine};
use shopify_client::types::product::{ProductQueryParams, ProductStatus};

let client = ShopifyClient::new(shop_url, access_token, None);

// 1. Start the export
let params = ProductQueryParams {
    status: Some(ProductStatus::Active),
    ..Default::default()
};
let resp = client.bulk_operation.export_products(Some(&params)).await?;
let op = resp.bulk_operation_run_query.bulk_operation.ok_or("no operation")?;

// 2. Poll until Shopify finishes processing
let url = loop {
    let status = client.bulk_operation.get(&op.id).await?;
    match status.bulk_operation.as_ref().map(|o| &o.status) {
        Some(BulkOperationStatus::Completed) => {
            break status.bulk_operation.and_then(|o| o.url).ok_or("no url")?;
        }
        Some(BulkOperationStatus::Failed) => return Err("export failed".into()),
        _ => tokio::time::sleep(std::time::Duration::from_secs(2)).await,
    }
};

// 3. Stream results — constant memory, processes batch_size items at a time
client.bulk_operation.stream_products(&url, 50, |batch| async move {
    // batch: Vec<ProductExportLine> with up to 50 items
    for item in &batch {
        match item {
            ProductExportLine::Product(p) => println!("Product: {}", p.title),
            ProductExportLine::Variant(v) => println!("  Variant: {:?}", v.sku),
            ProductExportLine::Media(m)   => println!("  Media: {:?}", m.media_content_type),
        }
    }
    Ok(())
}).await?;

Each prebuilt export has a matching stream_* method that takes a batch_size and an async callback:

Export Stream Line Enum
export_products stream_products ProductExportLine
export_orders stream_orders OrderExportLine
export_collections stream_collections CollectionExportLine
export_customers stream_customers CustomerExportLine
export_inventory_items stream_inventory_items InventoryItemExportLine
export_draft_orders stream_draft_orders DraftOrderExportLine

All stream_* methods accumulate up to batch_size parsed items before invoking your async callback. The last batch may contain fewer items. Lines that fail to parse are silently skipped.

// Process 100 products at a time — great for DB writes, API calls, S3 uploads
client.bulk_operation.stream_products(&url, 100, |batch| async move {
    save_to_db(&batch).await?;
    Ok(())
}).await?;

Custom Bulk Queries

For queries the templates don't cover, use run_query with your own GraphQL and stream_jsonl to consume the result:

let query = r#"{
  products {
    edges {
      node { id title tags }
    }
  }
}"#;

client.bulk_operation.run_query(query, None).await?;
// ... poll until complete, get the url ...

// Stream raw JSON lines in batches of 100
client.bulk_operation.stream_jsonl(&url, 100, |lines| async move {
    write_to_file(&lines).await?;
    Ok(())
}).await?;

If you already have JSONL content in memory (e.g. read from a local file), each export enum has a parse_line() method:

use shopify_client::types::bulk_operation::ProductExportLine;

let parsed = ProductExportLine::parse_line(line)?;

Webhooks

Parse Shopify compliance webhooks with type safety:

use shopify_client::webhooks::{parse_webhook_with_header, WebhookPayload};

match parse_webhook_with_header(topic_header, payload) {
    Ok(WebhookPayload::CustomersDataRequest(data)) => { /* ... */ }
    Ok(WebhookPayload::CustomersRedact(data))      => { /* ... */ }
    Ok(WebhookPayload::ShopRedact(data))            => { /* ... */ }
    Err(e) => eprintln!("Parse error: {:?}", e),
}

HMAC verification helpers live in shopify_client::webhooks::verify:

use shopify_client::webhooks::verify::{verify_hmac, verify_hmac_from_headers};

if !verify_hmac_from_headers(headers, body, app_secret) {
    return Err("invalid webhook signature");
}

Storefront API

Requires the storefront Cargo feature: shopify-client = { version = "0.19", features = ["storefront"] }

The Storefront API is what custom storefronts, BFFs, edge functions, and headless commerce backends use to read the public catalog and manage shopper carts. Requests use a Storefront access token (different from the Admin token) sent as X-Shopify-Storefront-Access-Token, against the /api/{version}/graphql.json endpoint.

Where to get a Storefront access token: create one in the Shopify Admin UI under Apps → Headless / Custom Storefront, or programmatically via client.storefront_access_token.create on the Admin API.

Getting Started

[dependencies]
shopify-client = { version = "0.19", features = ["storefront"] }
use shopify_client::storefront::ShopifyStorefront;

#[tokio::main]
async fn main() {
    let store = ShopifyStorefront::new(
        "https://your-shop.myshopify.com".to_string(),
        "your-storefront-access-token".to_string(),
        None, // defaults to API version 2026-01
    );

    let product = store.product.get_by_handle("my-product").await;
}

API Coverage

Service Field on ShopifyStorefront Operations
store.product Products get_by_id, get_by_handle, get_many, get_recommendations
store.collection Collections get_by_id, get_by_handle, get_with_products, get_many
store.cart Cart get, create, add_lines, update_lines, remove_lines, update_note, update_attributes, update_buyer_identity, update_discount_codes, add_gift_card_codes (plus *_without_customer variants)
store.customer Customers get, login, renew_token, logout, create, update, recover, reset, reset_by_url, activate, activate_by_url, create_address, update_address, delete_address, set_default_address
store.search Search search, predictive
store.content Pages / Blogs / Articles / Menus nested: content.pages.{get_by_id, get_by_handle, get_many}, content.blogs.{get_by_id, get_by_handle, get_many, get_with_articles}, content.articles.{get_by_id, get_many}, content.menus.get_by_handle
store.shop Shop info get, get_localization
store.metaobject Metaobjects get_by_id, get_by_handle, get_many

All Storefront types — products, carts, customers, response wrappers, input types, sort key enums, etc. — are generated from YAML specs in types/storefront/ (see Code Generation) and live under shopify_client::storefront::generated::types::*.

Examples

Products

use shopify_client::storefront::ShopifyStorefront;
use shopify_client::storefront::generated::types::products::{
    GetProductsArgs, GetProductRecommendationsArgs, ProductSortKeys, ProductRecommendationIntent,
};

let store = ShopifyStorefront::new(shop_url, storefront_token, None);

// Single product by handle (most common)
let resp = store.product.get_by_handle("classic-t-shirt").await?;
if let Some(product) = resp.product {
    println!("{}{}", product.title, product.handle);
}

// Single product by GID
let resp = store.product.get_by_id("gid://shopify/Product/123456").await?;

// Paginated list with filters
let resp = store.product.get_many(GetProductsArgs {
    first: Some(20),
    after: None,
    last: None,
    before: None,
    reverse: Some(false),
    sort_key: Some(ProductSortKeys::BestSelling),
    query: Some("tag:new".to_string()),
}).await?;

if let Some(conn) = resp.products {
    for node in conn.nodes {
        println!("{} ({})", node.title, node.handle);
    }
    if conn.page_info.has_next_page {
        // Use conn.page_info.end_cursor as `after` on the next call
    }
}

// Product recommendations (related, complementary)
let resp = store.product.get_recommendations(GetProductRecommendationsArgs {
    product_id: "gid://shopify/Product/123456".to_string(),
    intent: Some(ProductRecommendationIntent::Related),
}).await?;

Collections

use shopify_client::storefront::collection::CollectionRef;
use shopify_client::storefront::generated::types::collections::{
    GetCollectionsArgs, GetCollectionProductsArgs, ProductCollectionSortKeys, CollectionSortKeys,
};

// Single collection
let resp = store.collection.get_by_handle("summer-sale").await?;
let resp = store.collection.get_by_id("gid://shopify/Collection/789").await?;

// Collection with paginated products inside it (one round-trip)
let resp = store.collection.get_with_products(
    CollectionRef::Handle("summer-sale".to_string()),
    GetCollectionProductsArgs {
        first: Some(50),
        after: None,
        last: None,
        before: None,
        reverse: None,
        sort_key: Some(ProductCollectionSortKeys::BestSelling),
        filters: None,
    },
).await?;

// Paginated list of all collections
let resp = store.collection.get_many(GetCollectionsArgs {
    first: Some(20),
    after: None,
    last: None,
    before: None,
    reverse: None,
    sort_key: Some(CollectionSortKeys::Title),
    query: None,
}).await?;

Cart

The cart API has 10 mutations plus a get query, each with a *_without_customer variant for unauthenticated flows that omit buyerIdentity.customer from the response.

use shopify_client::storefront::generated::types::cart::{
    CartInput, CartLineInput, CartLineUpdateInput, CartBuyerIdentityInput,
};
use shopify_client::storefront::generated::types::common::AttributeInput;

// Create an empty cart
let resp = store.cart.create(CartInput {
    lines: None,
    note: None,
    attributes: None,
    buyer_identity: None,
    discount_codes: None,
    gift_card_codes: None,
    metafields: None,
}).await?;
let cart_id = resp.cart_create
    .and_then(|r| r.cart)
    .map(|c| c.id)
    .ok_or("cart creation failed")?;

// Add lines
let resp = store.cart.add_lines(&cart_id, vec![
    CartLineInput {
        merchandise_id: "gid://shopify/ProductVariant/111".to_string(),
        quantity: Some(2),
        attributes: None,
        selling_plan_id: None,
    },
]).await?;

// Update line quantities
store.cart.update_lines(&cart_id, vec![
    CartLineUpdateInput {
        id: "gid://shopify/CartLine/xyz".to_string(),
        quantity: Some(5),
        merchandise_id: None,
        attributes: None,
        selling_plan_id: None,
    },
]).await?;

// Apply a discount code
store.cart.update_discount_codes(&cart_id, vec!["SUMMER10".to_string()]).await?;

// Attach a logged-in customer
store.cart.update_buyer_identity(&cart_id, CartBuyerIdentityInput {
    email: Some("shopper@example.com".to_string()),
    phone: None,
    country_code: None,
    customer_access_token: Some(customer_token),
}).await?;

// Cart attributes (free-form key/value)
store.cart.update_attributes(&cart_id, vec![
    AttributeInput { key: "gift_wrap".to_string(), value: "true".to_string() },
]).await?;

// Read the latest state — `checkout_url` is what you redirect the shopper to
let resp = store.cart.get(&cart_id).await?;
if let Some(cart) = resp.cart {
    println!("Checkout: {}", cart.checkout_url);
}

Unauthenticated flows. Every cart method has a *_without_customer variant that uses a Cart fragment which omits buyerIdentity.customer. Use these when you don't have (and don't want to leak the existence of) a customer access token:

// Same shape as above, but the response Cart never includes buyerIdentity.customer
let resp = store.cart.create_without_customer(CartInput { /**/ }).await?;
store.cart.add_lines_without_customer(&cart_id, lines).await?;
store.cart.update_discount_codes_without_customer(&cart_id, vec!["SUMMER10".into()]).await?;

Customer

The customer API covers the full account lifecycle — signup, login, password reset, address management, order history.

use shopify_client::storefront::generated::types::customer::{
    CustomerAccessTokenCreateInput, CustomerCreateInput, CustomerUpdateInput,
    CustomerResetInput, CustomerActivateInput, MailingAddressInput,
};

// Sign up
let resp = store.customer.create(CustomerCreateInput {
    email: "new@shopper.com".to_string(),
    password: "supers3cret".to_string(),
    first_name: Some("Alex".to_string()),
    last_name: Some("Doe".to_string()),
    phone: None,
    accepts_marketing: Some(true),
}).await?;

// Log in — returns an access token + expiry
let resp = store.customer.login(CustomerAccessTokenCreateInput {
    email: "alex@shopper.com".to_string(),
    password: "supers3cret".to_string(),
}).await?;
let token = resp.customer_access_token_create
    .and_then(|r| r.customer_access_token)
    .map(|t| t.access_token)
    .ok_or("login failed")?;

// Renew before expiry
let resp = store.customer.renew_token(&token).await?;

// Fetch the customer + first N addresses + first N orders
let resp = store.customer.get(&token, Some(10), Some(20)).await?;
if let Some(customer) = resp.customer {
    println!("Hello, {}", customer.display_name);
}

// Update profile
store.customer.update(&token, CustomerUpdateInput {
    first_name: Some("Alexandra".to_string()),
    last_name: None,
    email: None,
    phone: None,
    password: None,
    accepts_marketing: None,
}).await?;

// Password recovery → email flow
store.customer.recover("alex@shopper.com").await?;

// After the user clicks the reset link, complete with reset_by_url
let resp = store.customer.reset_by_url(reset_url, "newp@ssword").await?;

// Or reset with an explicit id + reset token (e.g. from a deeplink param)
let resp = store.customer.reset(&customer_id, CustomerResetInput {
    reset_token: reset_token.clone(),
    password: "newp@ssword".to_string(),
}).await?;

// Account activation (for accounts created in admin without a password)
let resp = store.customer.activate(&customer_id, CustomerActivateInput {
    activation_token: activation_token.clone(),
    password: "newp@ssword".to_string(),
}).await?;

// Addresses
store.customer.create_address(&token, MailingAddressInput {
    first_name: Some("Alex".to_string()),
    last_name: Some("Doe".to_string()),
    address1: Some("1 Infinite Loop".to_string()),
    address2: None,
    city: Some("Cupertino".to_string()),
    province: Some("CA".to_string()),
    country: Some("United States".to_string()),
    zip: Some("95014".to_string()),
    phone: None,
    company: None,
}).await?;
store.customer.update_address(&token, &address_id, address).await?;
store.customer.delete_address(&token, &address_id).await?;
store.customer.set_default_address(&token, &address_id).await?;

// Log out (revoke the token)
store.customer.logout(&token).await?;

Search

use shopify_client::storefront::generated::types::search::{
    SearchArgs, PredictiveSearchArgs, SearchSortKeys, SearchType,
    SearchPrefixQueryType, SearchUnavailableProductsType,
    PredictiveSearchLimitScope, PredictiveSearchType,
};

// Full search (returns Product | Page | Article in a connection)
let resp = store.search.search(SearchArgs {
    query: "summer dress".to_string(),
    first: Some(20),
    after: None,
    last: None,
    before: None,
    reverse: None,
    sort_key: Some(SearchSortKeys::Relevance),
    types: Some(vec![SearchType::Product]),
    product_filters: None,
    prefix: Some(SearchPrefixQueryType::Last),
    unavailable_products: Some(SearchUnavailableProductsType::Hide),
}).await?;

// Predictive search — for instant-results UIs / typeahead
let resp = store.search.predictive(PredictiveSearchArgs {
    query: "sum".to_string(),
    limit: Some(5),
    limit_scope: Some(PredictiveSearchLimitScope::Each),
    types: Some(vec![PredictiveSearchType::Product, PredictiveSearchType::Collection]),
    unavailable_products: Some(SearchUnavailableProductsType::Hide),
}).await?;

if let Some(result) = resp.predictive_search {
    for p in result.products.unwrap_or_default() {
        println!("Product: {}", p.title);
    }
    for c in result.collections.unwrap_or_default() {
        println!("Collection: {}", c.title);
    }
}

Content

The content service is split into four sub-services matching Shopify's content model: pages, blogs, articles, and menus.

use shopify_client::storefront::content::BlogRef;
use shopify_client::storefront::generated::types::content::{
    GetPagesArgs, GetBlogsArgs, GetArticlesArgs, GetBlogArticlesArgs,
    PageSortKeys, BlogSortKeys, ArticleSortKeys,
};

// Pages
let resp = store.content.pages.get_by_handle("about").await?;
let resp = store.content.pages.get_by_id("gid://shopify/Page/1").await?;
let resp = store.content.pages.get_many(GetPagesArgs {
    first: Some(20),
    after: None, last: None, before: None,
    reverse: None,
    sort_key: Some(PageSortKeys::Title),
    query: None,
}).await?;

// Blogs
let resp = store.content.blogs.get_by_handle("news").await?;
let resp = store.content.blogs.get_many(GetBlogsArgs {
    first: Some(10),
    after: None, last: None, before: None,
    reverse: None,
    sort_key: Some(BlogSortKeys::Handle),
    query: None,
}).await?;

// Blog + its articles in one round-trip
let resp = store.content.blogs.get_with_articles(
    BlogRef::Handle("news".to_string()),
    GetBlogArticlesArgs {
        articles_first: Some(10),
        articles_after: None,
        articles_reverse: Some(true),
        articles_sort_key: Some(ArticleSortKeys::PublishedAt),
    },
).await?;

// Articles (top-level, across all blogs)
let resp = store.content.articles.get_by_id("gid://shopify/Article/42").await?;
let resp = store.content.articles.get_many(GetArticlesArgs {
    first: Some(20),
    after: None, last: None, before: None,
    reverse: Some(true),
    sort_key: Some(ArticleSortKeys::PublishedAt),
    query: Some("tag:featured".to_string()),
}).await?;

// Navigation menus
let resp = store.content.menus.get_by_handle("main-menu").await?;
if let Some(menu) = resp.menu {
    for item in menu.items {
        println!("{}{:?}", item.title, item.url);
    }
}

Shop & Localization

let resp = store.shop.get().await?;
if let Some(shop) = resp.shop {
    println!("{}", shop.name);
    println!("Currency: {}", shop.payment_settings.currency_code);
    println!("Ships to: {:?}", shop.ships_to_countries);
}

// Available languages, currencies, and country shopping experiences
let resp = store.shop.get_localization().await?;
if let Some(loc) = resp.localization {
    println!("Current country: {} ({})",
        loc.country.name, loc.country.iso_code);
    for lang in loc.available_languages {
        println!("  {}{}", lang.iso_code, lang.name);
    }
}

Metaobjects

use shopify_client::storefront::generated::types::metafields::{
    MetaobjectHandleInput, GetMetaobjectsArgs,
};

// By GID
let resp = store.metaobject.get_by_id("gid://shopify/Metaobject/1").await?;

// By {type, handle} tuple
let resp = store.metaobject.get_by_handle(MetaobjectHandleInput {
    handle: "homepage-banner".to_string(),
    type_: "banner".to_string(),
}).await?;

// All metaobjects of a given type, paginated
let resp = store.metaobject.get_many(GetMetaobjectsArgs {
    type_: "banner".to_string(),
    first: Some(20),
    after: None, last: None, before: None,
    reverse: None,
    sort_key: None,
}).await?;

Storefront vs Admin: when to use which

Need API Service
Display the catalog on a custom storefront Storefront store.product, store.collection
Manage a shopper's cart and redirect to checkout Storefront store.cart
Customer signup / login / address book Storefront store.customer
Process an order after it's placed Admin client.order
Run a one-off export of all products Admin client.bulk_operation.export_products
Charge a subscription Admin client.subscription
Define a discount that affects all storefronts Admin client.discount
Issue a Storefront access token for a new headless app Admin client.storefront_access_token.create

Request Callbacks

Both clients support optional before/after hooks on every request — useful for logging, metrics, and tracing.

use shopify_client::ShopifyClient;
use std::sync::Arc;

let client = ShopifyClient::new_with_callbacks(
    shop_url, admin_token, None,
    Some(Arc::new(|url, body, _headers| {
        println!("-> {} ({} bytes)", url, body.map_or(0, |b| b.len()));
    })),
    Some(Arc::new(|url, resp, _headers| {
        println!("<- {} ({} bytes)", url, resp.len());
    })),
);

ShopifyStorefront::new_with_callbacks has the exact same signature. Properties:

  • Fires on every REST and GraphQL request.
  • The shop-level access token is never passed to callbacks.
  • The request body can contain other sensitive values that travel as GraphQL variables — most notably a customer access token on storefront cart/customer mutations, and passwords on customerCreate / customerReset. Treat the body as sensitive; avoid logging it verbatim to shared destinations.
  • Panic-safe — a panicking callback won't crash the request flow.

Error Handling

All methods on both clients return Result<T, APIError>:

pub enum APIError {
    ServerError { errors: String },  // Shopify returned a GraphQL/REST error
    FailedToParse,                   // Response couldn't be deserialized
    NetworkError,                    // Connection / timeout failure
}

For GraphQL errors, APIError::ServerError.errors includes Shopify's error code where available — e.g. [THROTTLED] Throttled — so callers can classify failures for retry without parsing free text.

For Storefront mutations that return per-field validation errors (e.g. customerCreate with a bad email), check the customer_user_errors / user_errors field on the response payload — those are part of a successful response, not an APIError.

Code Generation

Storefront types are generated from YAML specs in types/storefront/ via type-crafter, invoked through npx:

make gen-storefront     # regenerate src/storefront/generated/types/ from the YAML specs
make clean-storefront   # wipe generated output

The generated output is checked into git, so cargo build works without Node installed. Regenerate after editing any types/storefront/*.yaml file.

Project Structure

shopify-rust-client/
├── Cargo.toml                       # single [package]; `storefront` feature
├── Makefile                         # `make gen-storefront` regenerates storefront types
├── types/storefront/*.yaml          # source specs for the generated storefront types
└── src/
    ├── lib.rs                       # ShopifyClient
    ├── common/                      # shared infra
    │   ├── types.rs                 #   APIError, RequestCallbacks, PageInfo, …
    │   ├── http.rs                  #   shared reqwest client + GraphQL executors
    │   ├── utils.rs                 #   parse_response helpers
    │   ├── query_filter.rs          #   DateFilter, NumericFilter
    │   └── mod.rs                   #   ServiceContext
    ├── admin/                       # admin services (also re-exported as `services`)
    │   └── <service>/{mod.rs, remote.rs}   # order, subscription, discount, …, bulk_operation
    ├── types/                       # admin request/response types
    ├── webhooks/                    # compliance webhook parsing + HMAC verify
    └── storefront/                  # behind the `storefront` feature
        ├── mod.rs                   #   ShopifyStorefront
        ├── fragments/               #   GraphQL fragments shared across services
        ├── <service>/               #   product, cart, collection, customer, …
        │   ├── mod.rs               #     service struct + async methods
        │   ├── remote.rs            #     internal HTTP calls
        │   └── queries.rs           #     GraphQL query builders
        └── generated/types/         #   type-crafter output (checked in)

Requirements

  • Rust 2021 edition (1.70+)
  • Always: reqwest, serde, serde_json, hmac, sha2, base64
  • Only with the storefront feature: time (typed OffsetDateTime timestamps on generated Storefront types)

Contributing

Contributions are welcome! Please feel free to submit a Pull Request. If you change any types/storefront/*.yaml spec, run make gen-storefront and commit the regenerated output.

License

This project is licensed under the MIT License.


This is an unofficial client library. For official Shopify documentation, visit shopify.dev.