Claude CodeでShopifyをGraphQLへ移行 — REST呼び出しの置き換え手順
Shopify Admin APIのREST呼び出しをgrepで棚卸しし、Claude CodeにGraphQLへ書き換えさせ、同じ商品をRESTとGraphQLの両方で取って差分を照合する移行手順をまとめます。
Shopify Admin APIのREST呼び出しをGraphQLへ置き換える作業は、Claude Codeに向いています。コード内の /admin/api/ 呼び出しを機械的に洗い出せて、書き換えの正しさも「同じデータが返るか」で確かめられるからです。この記事では、棚卸し、書き換え、差分照合の3段階で進める手順を、そのまま使えるコマンドとスクリプトで示します。
なぜ今RESTを置き換えるのか
REST Admin APIはレガシーAPIです。2024年10月1日にレガシー扱いとなり、2025年4月1日以降に作る新規の公開アプリはGraphQL Admin APIだけで作る決まりになっています。新しい機能はGraphQLにしかない場合もあります。
既存の自社アプリやスクリプトがRESTで動いているなら、すぐ止まるわけではありません。ただ、新機能に触れるたびにGraphQL側の書き方が要るので、移行の先送りは負債として積み上がります。
REST呼び出しとGraphQL呼び出しの違いは、書き換えの前に押さえておくと迷いません。
| 観点 | REST Admin API | GraphQL Admin API |
|---|---|---|
| エンドポイント | REST Admin API/admin/api/2026-10/{resource}.json | GraphQL Admin API/admin/api/2026-10/graphql.json に POST |
| 返すフィールド | REST Admin APIリソース単位で固定 | GraphQL Admin APIクエリで選んだフィールドだけ |
| レート制限 | REST Admin APIアプリ・ストアあたり毎分40リクエスト(Plusは10倍) | GraphQL Admin APIクエリのコスト点数で計算 |
| エラーの見え方 | REST Admin API4xx・5xxの状態コード | GraphQL Admin API4xx相当でも 200 OK と errors で返ることがある |
| ID | REST Admin API数値ID | GraphQL Admin APIgid://shopify/Product/123 形式 |
手順1: REST呼び出しを棚卸しする
まず書き換え対象を一覧にします。ここは推測で進めず、grepの結果を台帳にします。
grep -rnE "/admin/api/[^\"' ]*\.json|admin\.rest\.|clients\.Rest|Clients::Rest" \
--include='*.js' --include='*.ts' --include='*.rb' --include='*.py' src/ scripts/ | tee rest-inventory.txtこの正規表現は、URL直書きの /admin/api/…json と、公式クライアントライブラリのREST経由の呼び出し(admin.rest.get、clients.Rest、Clients::Rest::Admin)の両方を拾います。拡張子や検索ディレクトリはプロジェクトに合わせて変えてください。
件数が多いときは、Claude Codeに非対話モードで分類させると台帳が整います。--json-schema を付ければ、結果が決まった形のJSONで返ります。
claude -p "rest-inventory.txt の各行を読み、呼び出しごとにリソース名、\
HTTPメソッド、該当ファイルと行をまとめて" \
--allowedTools "Read" \
--output-format json \
--json-schema '{"type":"object","properties":{"calls":{"type":"array","items":{"type":"object","properties":{"resource":{"type":"string"},"method":{"type":"string"},"file":{"type":"string"},"line":{"type":"integer"}},"required":["resource","method","file","line"]}}},"required":["calls"]}' \
| jq '.structured_output.calls'structured_output は、有効な構造化出力が返らなかった実行では含まれません。スクリプトに組み込むときは、フィールドの有無を先に確かめてください。Read だけを許可しているので、この実行では何も書き換わりません。
手順2: 権限とCLAUDE.mdで書き換えの範囲を縛る
書き換え中のClaude Codeが、本番ストアへ書き込みを送ってしまうと取り返しがつきません。次の2つを先に置きます。
.claude/settings.json では、移行用の照合スクリプトだけを許可し、通信系のコマンドは止めます。
{
"permissions": {
"allow": [
"Bash(node scripts/parity-check.mjs *)",
"Bash(npm run test *)"
],
"deny": [
"Bash(curl *)"
]
}
}権限ルールは「deny、ask、allow」の順に評価され、先に当たったものが優先されます。Bash(curl *) を拒否にしておけば、許可ルールが広くても curl は通りません。トークンを持つ実行は、許可した照合スクリプトの中だけに限られます。
CLAUDE.md には移行の約束事を書きます。
## Shopify REST から GraphQL への移行
- 対象は rest-inventory.txt に載った呼び出しだけ。載っていない箇所は触らない
- 変更は1リソース(products など)ずつ。1リソースごとに照合を通してから次へ進む
- REST の数値IDは `gid://shopify/<Type>/<id>` に変換する
- GraphQL の呼び出しは、HTTP ステータスに加えて `errors` を必ず確認する
- 書き込み系は `userErrors` を要求し、空であることを確認する
- 本番ストアに対する書き込みは実行しない。照合は開発ストアで行う最後の1行は、約束事というより運用の線引きです。誤って本番のトークンを渡さないよう、リポジトリには資格情報を置かず、実行する端末の環境変数だけで渡す形にします。照合用の資格情報は開発ストアのものだけを環境変数で渡してください。
手順3: リソースごとにGraphQLへ書き換える
棚卸しの先頭、たとえば products から1リソースずつClaudeに頼みます。指示は「このファイルのこの呼び出しを、同じ値が取れるGraphQLクエリに置き換える」と具体的に書きます。
商品1件の取得は、RESTの GET /admin/api/2026-10/products/{product_id}.json が、GraphQLの product(id:) クエリに対応します。
query ProductById($id: ID!) {
product(id: $id) {
id
title
variants(first: 50) {
edges {
node {
id
title
}
}
}
}
}書き換え時に注意するのは、次の3点です。
- IDの変換。RESTのレスポンスにはGraphQL用のIDが
admin_graphql_api_idとして入っています。数値IDから組み立てる場合はgid://shopify/Product/<数値ID>の形にします - 階層の違い。バリエーションはRESTでは
product.variantsの配列ですが、GraphQLではedgesとnodeを経由する接続です。読み取り側のコードも一緒に直します - 件数の指定。
variants(first: 50)のように取る件数を明示します。大量取得の設計は、GraphQLのバルク処理など別の話題になるので、ここでは扱いません
商品の更新は、productUpdateミューテーションに対応づけます。ただしproductUpdateはバリエーションを更新できず、価格などはproductVariantsBulkUpdateで別に呼びます。RESTで1回だった更新が、2回の呼び出しに分かれることがあります。外部のデータベースをShopifyへ丸ごと同期する処理なら、商品の状態をまとめて宣言するproductSetが向いています。書き込み系ではuserErrorsを必ず要求してください。ミューテーションの詳細なエラーは、これを要求したときに返ります。
手順4: 同じデータが返るかを差分で照合する
書き換えたコードが正しいかどうかは、RESTとGraphQLに同じ商品を取らせて比べるのが確実です。次のスクリプトを scripts/parity-check.mjs として置きます。開発ストアの値は環境変数から読みます。
// scripts/parity-check.mjs 使い方: node scripts/parity-check.mjs <商品の数値ID>
const shop = process.env.SHOPIFY_STORE; // 例: your-dev-store.myshopify.com
const token = process.env.SHOPIFY_ACCESS_TOKEN;
const base = `https://${shop}/admin/api/2026-10`;
const headers = {
"Content-Type": "application/json",
"X-Shopify-Access-Token": token,
};
const id = process.argv[2];
const rest = await fetch(`${base}/products/${id}.json`, { headers })
.then((r) => r.json());
const query = `query($id: ID!) {
product(id: $id) { title variants(first: 50) { edges { node { id } } } }
}`;
const gql = await fetch(`${base}/graphql.json`, {
method: "POST",
headers,
body: JSON.stringify({
query,
variables: { id: rest.product.admin_graphql_api_id },
}),
}).then((r) => r.json());
if (gql.errors) {
console.error("GraphQL errors:", JSON.stringify(gql.errors));
process.exit(1);
}
const gotRest = {
title: rest.product.title,
variants: rest.product.variants.length,
};
const gotGql = {
title: gql.data.product.title,
variants: gql.data.product.variants.edges.length,
};
console.log("REST :", gotRest);
console.log("GraphQL:", gotGql);
process.exit(JSON.stringify(gotRest) === JSON.stringify(gotGql) ? 0 : 1);GraphQLは 200 OK のまま errors を返すことがあるため、スクリプトは状態コードでなく errors の有無で失敗を判定しています。照合の終了コードが0かどうかだけで合否が決まるので、Claudeは次のように反復できます。
claude -p "products の移行後コードに対して \
node scripts/parity-check.mjs 123456789 を実行し、\
終了コードが1なら差分を読んでコードを直し、0になるまで繰り返して" \
--allowedTools "Read,Edit,Bash(node scripts/parity-check.mjs *)"123456789 は開発ストアにある商品の数値IDに置き換えます。許可したのは Read・Edit と照合スクリプトの実行だけなので、ほかのコマンドは自動では通りません。
落とし穴
照合が通っても、取得項目の取りこぼしは見つからない
このスクリプトが比べるのは、タイトルとバリエーション数だけです。アプリが実際に読んでいるフィールド(価格、在庫、メタフィールドなど)が増えたら、比較対象も同じだけ増やします。照合の項目は、手順1の台帳でコードが読んでいるフィールドから起こすと取りこぼしを減らせます。
エラー処理をステータスコードからerrorsへ移す
RESTのコードは、429や401などの状態コードで分岐しているのが普通です。GraphQLでは同じ状況が 200 OK のまま errors 配列の extensions.code で返ることがあるため、分岐の置き場所が変わります。対応は次のとおりです。
GraphQLの extensions.code | RESTでの近い状況 |
|---|---|
THROTTLED | RESTでの近い状況429 Too Many Requests |
ACCESS_DENIED | RESTでの近い状況401 Unauthorized |
SHOP_INACTIVE | RESTでの近い状況ストアが無効(レート制限の超過を繰り返したり、不正のリスクがあったりすると起こる) |
INTERNAL_SERVER_ERROR | RESTでの近い状況500 Internal Server Error(多くの場合、500の代わりにこちらで返る) |
RESTの429には Retry-After ヘッダーが付き、待つ秒数が分かりました。GraphQLでは、成功したレスポンスの extensions.cost.throttleStatus に、上限の maximumAvailable、残りの currentlyAvailable、回復速度の restoreRate が入ります。再試行の待ち時間は、この値から計算する形に直します。
書き換え済みのコードに、次のような確認を入れるようClaudeに頼むと、状態コードだけを見ている箇所が残りません。
const body = await res.json();
if (body.errors?.some((e) => e.extensions?.code === "THROTTLED")) {
// 残りのコスト点数を見て待ってから再試行する
}
if (body.errors) throw new Error(JSON.stringify(body.errors));移行後のテストには、errors を含む 200 のレスポンスを返すモックを1本足しておくと、エラー処理の置き換え漏れを検出できます。
移行の途中で残る確認点
照合を通っても、次の5点はコードを読んで確かめます。
- レート制限の考え方が変わる。RESTは毎分40リクエスト(Plusは10倍)、GraphQLはクエリのコスト点数です。ループで1件ずつ取っていた処理を、1クエリにまとめられるか見直す余地があります。レート制限に合わせて呼び出しを間引く移行スクリプトの考え方は、Notion APIのレート制限をClaude Codeで守る例が参考になります
- 古いバージョンの呼び出しが残っていないか。URLに含まれるバージョンは
2026-10のように固定されます。移行中は新旧が混在するので、期限を決めて旧バージョンの呼び出しを廃止する流れはSunsetヘッダで旧APIを廃止予告する手順が参考になります - バリエーション作成の日次上限は、APIを変えても残る。ストアのバリエーションが50万件に達すると、新規作成は1日1万件までに制限されます。この制限はどのAPIにも共通で、GraphQLへ移しても回避にはなりません。Shopify Plusストアには適用されません。商品の一括登録スクリプトを移すなら、先にストアの規模を確認してください
- クエリの試し書きはGraphiQLで。書き換え前のクエリをClaudeに作らせる前に、ShopifyのGraphiQLアプリで実データに対して動かしておくと、フィールド名の誤りを早く見つけられます。見つけたクエリをそのままプロンプトに貼れば、書き換えの指示が具体的になります
- 型付けとスキーマ検証。GraphQLのクエリが増えたら、生成された型で壊れ方を早く見つけられます。設定はClaude CodeでGraphQL Codegenを使う手順にまとめています
Shopifyの開発自体をClaude Codeに寄せたいなら、Shopify AI Toolkitの入れ方も合わせて見ると、スキーマを参照しながら書かせる構成を組めます。GraphQLのAPIを試しにClaudeから叩かせたいだけなら、GraphQL MCPサーバーの使い方が近道です。
まとめ
RESTからGraphQLへの置き換えは、grepで台帳を作り、リソース単位で書き換え、RESTとGraphQLの差分を終了コードで判定する流れにすると、Claude Codeに任せやすくなります。照合スクリプトが「合っているか」を決めてくれるので、人が見るのは照合項目の足りなさと、資格情報を開発ストアに限る運用だけです。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。