shopify-storefront-api について
Storefront GraphQL APIでカスタムShopifyストアフロントを構築する開発者向けスキルです。トークン設定、商品やコレクションの取得、カート操作を説明し、ヘッドレスサイトやモバイルアプリに対応します。
主な機能
- 01Storefront Access Tokenの作成
- 02GraphQLによる商品・コレクション取得
- 03カート作成と明細管理
- 04カートIDの保存とチェックアウトへの遷移
- 05予測検索とバリエーション選択の実装
活用シーン
- 01Next.jsやRemixでヘッドレスストアを構築
- 02モバイルアプリで商品・カート情報を取得
- 03Shopify以外のサイトに購入ウィジェットを埋め込み
- 04セッションをまたぐカートの保存
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ finsilabs/ awesome-ecommerce-skills --skill shopify-storefront-apiインストール後、Claude Code で/shopify-storefront-apiを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
tile.jsonevals/cart-persistence-usererrors-handling-var/criteria.jsontask.mdevals/cursor-pagination-incontext-directive-fr/criteria.jsontask.mdevals/storefront-client-setup-with-public-and-/criteria.jsontask.md詳細
Shopify Storefront API
概要
Shopify Storefront API は、あらゆるフロントエンドからストアの商品、コレクション、カート、チェックアウトへの読み取りと書き込みを可能にする、公開向けの GraphQL API です。Storefront Access Token(Admin API トークンとは別のもの)を使用し、クライアント側の JavaScript で公開しても安全です。Next.js、Remix/Hydrogen、または任意の JS フレームワークを使用して、ヘッドレスストアフロントを構築できます。
このスキルを使用する場面
- カスタムのフロントエンドフレームワークでヘッドレス Shopify ストアフロントを構築する場合
- 商品やカートのデータが必要な React Native または Flutter のモバイルアプリを作成する場合
- Shopify 以外のサイトに Shopify の購入ボタンや商品ウィジェットを埋め込む場合
- Shopify Hydrogen(Remix ベース)で完全にカスタムなストアフロントを構築する場合
- Admin API のオーバーヘッドなしで、商品在庫や価格をリアルタイムに取得する場合
- Shopify のホスト型カートを使用して、セッションをまたいでカートを保持する場合
基本的な手順
-
Storefront Access Token を作成する
Shopify Admin → Apps → Develop apps → Your App → API credentials → Storefront API access token に移動します。または、Admin API を使用します。
// Via Admin API (one-time setup) const token = await admin.graphql(` mutation { storefrontAccessTokenCreate(input: { title: "Headless Frontend" }) { storefrontAccessToken { accessToken title } userErrors { field message } } } `);Storefront Access Token には
shpat_プレフィックスは付きません(このプレフィックスは Admin API トークン用です)。Storefront トークンはブラウザーのコードで安全に使用できる不透明な文字列であり、ストアフロントのスコープ内の操作のみを許可します。 -
Storefront API クライアントを設定する
公式の
@shopify/storefront-api-clientを使用します。npm install @shopify/storefront-api-client// lib/shopify.ts import { createStorefrontApiClient } from "@shopify/storefront-api-client"; export const storefront = createStorefrontApiClient({ storeDomain: process.env.NEXT_PUBLIC_SHOPIFY_STORE_DOMAIN!, // e.g. "mystore.myshopify.com" apiVersion: "2025-01", publicAccessToken: process.env.NEXT_PUBLIC_SHOPIFY_STOREFRONT_TOKEN!, });プライベートアクセストークンを使用したサーバーサイドの呼び出しの場合(レート制限が高くなります):
export const storefrontServer = createStorefrontApiClient({ storeDomain: process.env.SHOPIFY_STORE_DOMAIN!, apiVersion: "2025-01", privateAccessToken: process.env.SHOPIFY_STOREFRONT_PRIVATE_TOKEN!, }); -
商品とコレクションをクエリする
// lib/products.ts export async function getProducts(first = 20, after?: string) { const { data, errors } = await storefront.request(` query GetProducts($first: Int!, $after: String) { products(first: $first, after: $after, sortKey: BEST_SELLING) { pageInfo { hasNextPage endCursor } edges { node { id title handle availableForSale priceRange { minVariantPrice { amount currencyCode } maxVariantPrice { amount currencyCode } } images(first: 1) { edges { node { url altText width height } } } variants(first: 10) { edges { node { id title availableForSale selectedOptions { name value } price { amount currencyCode } } } } } } } } `, { variables: { first, after } }); if (errors) throw new Error(errors.message); return data.products; } -
カートを作成して管理する
// lib/cart.ts // Create a new cart export async function cartCreate(lines: { merchandiseId: string; quantity: number }[]) { const { data } = await storefront.request(` mutation CartCreate($lines: [CartLineInput!]) { cartCreate(input: { lines: $lines }) { cart { id checkoutUrl lines(first: 50) { edges { node { id quantity merchandise { ... on ProductVariant { id title price { amount currencyCode } product { title handle } } } } } } cost { subtotalAmount { amount currencyCode } totalAmount { amount currencyCode } } } userErrors { field message } } } `, { variables: { lines } }); return data.cartCreate; } // Add lines to existing cart export async function cartLinesAdd(cartId: string, lines: { merchandiseId: string; quantity: number }[]) { const { data } = await storefront.request(` mutation CartLinesAdd($cartId: ID!, $lines: [CartLineInput!]!) { cartLinesAdd(cartId: $cartId, lines: $lines) { cart { id checkoutUrl } userErrors { field message } } } `, { variables: { cartId, lines } }); return data.cartLinesAdd; } -
カート ID を保持してチェックアウトにリダイレクトする
// hooks/useCart.ts import { useState, useEffect } from "react"; import { cartCreate, cartLinesAdd } from "../lib/cart"; const CART_ID_KEY = "shopify_cart_id"; export function useCart() { const [cartId, setCartId] = useState<string | null>(null); const [checkoutUrl, setCheckoutUrl] = useState<string | null>(null); useEffect(() => { setCartId(localStorage.getItem(CART_ID_KEY)); }, []); const addToCart = async (variantId: string, quantity = 1) => { const lines = [{ merchandiseId: variantId, quantity }]; if (cartId) { const result = await cartLinesAdd(cartId, lines); setCheckoutUrl(result.cart.checkoutUrl); } else { const result = await cartCreate(lines); const newCartId = result.cart.id; localStorage.setItem(CART_ID_KEY, newCartId); setCartId(newCartId); setCheckoutUrl(result.cart.checkoutUrl); } }; const goToCheckout = () => { if (checkoutUrl) window.location.href = checkoutUrl; }; return { addToCart, goToCheckout, cartId }; }
例
バリエーション選択機能付きの商品詳細ページ(Next.js)
// app/products/[handle]/page.tsx
import { storefront } from "@/lib/shopify";
async function getProduct(handle: string) {
const { data } = await storefront.request(`
query GetProduct($handle: String!) {
product(handle: $handle) {
id
title
descriptionHtml
seo { title description }
images(first: 10) {
edges { node { url altText } }
}
options {
id name values
}
variants(first: 100) {
edges {
node {
id
availableForSale
selectedOptions { name value }
price { amount currencyCode }
compareAtPrice { amount currencyCode }
}
}
}
}
}
`, { variables: { handle } });
return data.product;
}
export default async function ProductPage({ params }: { params: { handle: string } }) {
const product = await getProduct(params.handle);
// Render product with client-side variant picker
return <ProductDetail product={product} />;
}
// Generate static params for all products
export async function generateStaticParams() {
const { data } = await storefront.request(`
query { products(first: 200) { edges { node { handle } } } }
`);
return data.products.edges.map(({ node }: { node: { handle: string } }) => ({
handle: node.handle,
}));
}
予測検索
export async function predictiveSearch(query: string) {
const { data } = await storefront.request(`
query PredictiveSearch($query: String!) {
predictiveSearch(query: $query, limit: 5, types: [PRODUCT, COLLECTION, ARTICLE]) {
products {
id title handle
featuredImage { url altText }
priceRange { minVariantPrice { amount currencyCode } }
}
collections {
id title handle
image { url altText }
}
}
}
`, { variables: { query } });
return data.predictiveSearch;
}
ベストプラクティス
- プライベートトークンはサーバーサイドで使用する — プライベート Storefront Access Token はレート制限が高く(100 req/s に対して 1000 req/s)、ブラウザーに決して公開しないでください
- 可能であればビルド時に商品データを取得する(ISR または SSG)— Storefront API のレート制限は顧客ごとではなく、ストアごとに適用されます
- 「カートに追加」を表示する前に、商品とバリエーションの両方で
availableForSaleを必ず確認する — 商品が販売可能でも、個々のバリエーションは売り切れている場合があります - オフセットではなく
afterカーソルを使ってページネーションする — Storefront API はカーソルベースのページネーションを使用します。次のページをクエリするためにendCursorを保存してください - Next.js の
fetchキャッシュタグまたは React cache を使って、コレクションや商品へのクエリをキャッシュする — 商品データがリアルタイムに変わることはほとんどありません - 国際価格には
@inContextディレクティブを使用する —@inContext(country: CA, language: EN)は購入者の通貨で価格を返します - フラグメントを再利用する — GraphQL フラグメント(例:
ProductFragment)を定義し、クエリ間でフィールド選択が重複しないようにします - すべてのミューテーションで
userErrorsを処理する — カートのミューテーションはuserErrors配列を返します。ローカル状態を更新する前に確認してください
よくある問題
| 問題 | 解決策 |
|---|---|
| レート制限エラー(429) | サーバーサイドでプライベートアクセストークンを使用し、リクエストのバッチ処理を実装します。商品クエリの N+1 問題を避けてください |
| ページを再読み込みするとカート ID が失われる | cartId を localStorage または Cookie に保存します。カートが存在しない場合にのみ、新しいカートを作成してください |
| 商品価格が間違った通貨で表示される | @inContext(country: $country) ディレクティブを追加し、位置情報を使って購入者の国を渡します |
product(handle:) が null を返す | スラッグ化されたハンドルを正しく処理します。Shopify のハンドルは小文字とハイフンで構成されます。スラッグが正確か確認してください |
| モバイル Safari でチェックアウトへのリダイレクトに失敗する | 非同期コールバックではなく、ユーザー操作のイベントハンドラー内で window.location.href = checkoutUrl を使用します |
| オプションの選択時にバリエーションが見つからない | 1 つだけでなく、すべての selectedOptions が一致する variants.edges をクライアント側で絞り込みます |
関連スキル
- @shopify-admin-api
- @shopify-app-development
- @shopify-checkout-extensions
- @headless-commerce-architecture
- @graphql-api-design