Skip to main content

Shopify Integration

Garmint integrates with Shopify to fetch garment blanks and product data for mockup generation.

Overview

Storefront API

Read-only access to products and variants

Admin API

Full product management (optional)

Configuration

Environment Variables

# Storefront API (read-only, safe for client)
NEXT_PUBLIC_SHOPIFY_STORE_DOMAIN=yourstore.myshopify.com
NEXT_PUBLIC_SHOPIFY_STOREFRONT_TOKEN=shpat_xxxxxxxxxxxxx

# Admin API (server-only, optional)
SHOPIFY_ADMIN_TOKEN=shpat_xxxxxxxxxxxxx

# API versions (optional)
SHOPIFY_STOREFRONT_API_VERSION=2024-07
SHOPIFY_ADMIN_API_VERSION=2024-07

Shopify Setup

  1. Go to your Shopify admin
  2. Settings → Apps and sales channels → Develop apps
  3. Create a new app
  4. Configure Storefront API scopes:
    • unauthenticated_read_product_listings
    • unauthenticated_read_product_inventory
  5. Install the app and copy tokens

Usage

Check Configuration

import { isShopifyConfigured } from "@/lib/shopify";

if (isShopifyConfigured()) {
  // Use Shopify data
  const products = await fetchProducts();
} else {
  // Fall back to mock data
  const products = getMockGarments();
}

Storefront Query

import { storefrontQuery } from "@/lib/shopify";

interface ProductsResponse {
  products: {
    edges: Array<{ node: ProductNode }>;
  };
}

const data = await storefrontQuery<ProductsResponse>(`
  query Products($first: Int!) {
    products(first: $first) {
      edges {
        node {
          id
          title
          description
          tags
          featuredImage { url }
          variants(first: 20) {
            edges {
              node {
                id
                price { amount currencyCode }
                availableForSale
                selectedOptions { name value }
              }
            }
          }
        }
      }
    }
  }
`, { first: 50 });

Admin Request (Server Only)

import { adminRequest } from "@/lib/shopify";

// Get product by ID
const product = await adminRequest<{ product: AdminProduct }>(
  `products/${productId}.json`
);

// Update product
await adminRequest(`products/${productId}.json`, {
  method: "PUT",
  body: JSON.stringify({
    product: { title: "New Title" }
  }),
});

Data Transformation

Garment Type

Shopify products are transformed to the Garment type:
interface Garment {
  id: string;
  title: string;
  handle: string;
  category: string;
  color: string;
  fabric?: string;
  previewUrl: string;
  variants: GarmentVariant[];
}

interface GarmentVariant {
  id: string;
  size: string;
  color?: string;
  price: number;
  available: boolean;
}

Transformation Function

function toGarment(node: StorefrontProductNode): Garment {
  return {
    id: node.id,
    title: node.title,
    handle: node.handle,
    category: getCategoryFromTags(node.tags),
    color: getColorFromOptions(node.variants.edges[0]?.node.selectedOptions),
    fabric: getFabricFromMetafield(node.metafields),
    previewUrl: node.featuredImage?.url ?? PLACEHOLDER_IMAGE,
    variants: node.variants.edges.map(({ node: v }) => ({
      id: v.id,
      size: getSizeFromOptions(v.selectedOptions),
      price: parseFloat(v.price.amount),
      available: v.availableForSale,
    })),
  };
}

Product Blocklist

Exclude specific products from display:
// lib/productBlocklist.ts
export const blockedHandles = new Set([
  "test-product",
  "sample-item",
  "do-not-show",
]);

export function isBlocked(handle: string): boolean {
  return blockedHandles.has(handle.toLowerCase());
}
Usage:
const products = await fetchProducts();
const filtered = products.filter(p => !isBlocked(p.handle));

Mock Data Fallback

Without Shopify credentials, use mock data:
// lib/mockGarments.ts
export const mockGarments: Garment[] = [
  {
    id: "mock-black-tee",
    title: "Classic Heavyweight Tee",
    handle: "classic-heavyweight-tee",
    category: "T-Shirts",
    color: "Black",
    fabric: "6.1 oz 100% Cotton",
    previewUrl: "/samples/black-tee.png",
    variants: [
      { id: "v1", size: "S", price: 24.00, available: true },
      { id: "v2", size: "M", price: 24.00, available: true },
      { id: "v3", size: "L", price: 24.00, available: true },
      { id: "v4", size: "XL", price: 26.00, available: true },
    ],
  },
  // More products...
];

Hooks

useShopifyGarments

// hooks/useShopifyGarments.ts
export function useShopifyGarments() {
  const [garments, setGarments] = useState<Garment[]>([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    async function load() {
      try {
        const res = await fetch("/api/products");
        const data = await res.json();
        setGarments(data.products);
      } catch (error) {
        console.error("Failed to load garments:", error);
        setGarments(getMockGarments());
      } finally {
        setLoading(false);
      }
    }
    load();
  }, []);

  return { garments, loading };
}

API Route

GET /api/products

Fetches products from Shopify or returns mocks:
// app/api/products/route.ts
export async function GET() {
  try {
    if (isShopifyConfigured()) {
      const products = await fetchShopifyProducts();
      return Response.json({ products });
    }
    
    return Response.json({ 
      products: getMockGarments(),
      source: "mock",
    });
  } catch (error) {
    return Response.json({ 
      error: "Failed to fetch products" 
    }, { status: 500 });
  }
}

GraphQL Queries

Fetch All Products

query Products($first: Int!, $query: String) {
  products(first: $first, query: $query) {
    edges {
      node {
        id
        title
        handle
        description
        tags
        productType
        createdAt
        featuredImage {
          url
          altText
        }
        options {
          name
          values
        }
        variants(first: 50) {
          edges {
            node {
              id
              title
              availableForSale
              quantityAvailable
              price {
                amount
                currencyCode
              }
              selectedOptions {
                name
                value
              }
              image {
                url
              }
            }
          }
        }
        metafields(identifiers: [
          { namespace: "custom", key: "fabric" },
          { namespace: "custom", key: "weight" }
        ]) {
          key
          value
        }
      }
    }
    pageInfo {
      hasNextPage
      endCursor
    }
  }
}

Fetch Single Product

query Product($handle: String!) {
  product(handle: $handle) {
    id
    title
    # ... same fields
  }
}

Error Handling

try {
  const data = await storefrontQuery(query, variables);
  return data;
} catch (error) {
  if (error.message.includes("not configured")) {
    // Fall back to mocks
    return { products: getMockGarments() };
  }
  
  console.error("Shopify query failed:", error);
  throw error;
}

Caching

Storefront queries are cached for 30 seconds:
const response = await fetch(url, {
  // ...
  next: { revalidate: 30 },
});
For real-time inventory, reduce or disable caching:
const response = await fetch(url, {
  // ...
  cache: 'no-store',
});

Best Practices

Use the Storefront API for client-facing product data. It’s read-only and safe to expose.
Never expose Admin API tokens to the client. Use them only in server-side code.
Test with mock data first to ensure your UI works, then connect Shopify for real products.