spree-typescript-sdk について
spree-typescript-sdk は、Spree 6 の Store、Admin、Seller API 向け TypeScript SDK 群です。ストアフロントや連携機能の開発者向けに、共通のリクエスト形式、リトライ、冪等性対応、任意の Zod スキーマを提供します。
主な機能
- 01Store、Admin、Seller API に対応
- 02リソース CRUD とドメイン操作
- 03トークン認証とリクエスト単位のオプション
- 04リトライ、エラー正規化、冪等性
- 05生成型と任意の Zod スキーマ
活用シーン
- 01Spree のストアフロントと決済フローの構築
- 02管理画面やセラー画面の開発
- 03連携機能からの Admin API 呼び出し
- 04API レスポンスの検証と Webhook 処理
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ spree/ agent-skills --skill spree-typescript-sdkインストール後、Claude Code で/spree-typescript-sdkを入力して読み込まれたか確認します。
詳細
Spree TypeScript SDK
| パッケージ | 対象 API | 認証 | バージョン |
|---|---|---|---|
@spree/sdk | Store API (/api/v3/store) | パブリッシャブルキー + 任意の顧客 JWT / ゲストトークン | 2.x(現在 2.0 ベータ) |
@spree/admin-sdk | Admin API (/api/v3/admin) | シークレットキー または スタッフ JWT | 1.x ベータ |
@spree/seller-sdk | Seller API (/api/v3/seller) | セラー JWT + セラー ID(createSellerClient、setSeller) | 1.x ベータ。シングルトンでは Admin SDK と同様に get/update を使用します(seller.me.get()、seller.me.update({ selected_locale })、seller.profile.update(…))。spree-marketplace を参照してください |
@spree/sdk-core | 共通の HTTP、リトライ、エラー | — | 内部用、公開されていません。インポートしないでください |
3 つの SDK はすべて同じ構成で、SpreeError、リトライ、冪等性、エスケープハッチとしての request を共有します。Spree 6 を対象としています。Spree 6 のストアフロントには @spree/sdk 2.x が必要です。6.0 が正式リリースされるまでは、SDK とダッシュボードのパッケージは npm の beta dist-tag でプレリリースとして公開されます(pnpm add @spree/sdk@beta)。一方、@spree/cli と create-spree-app は安定版です(gems 6.0.0.beta4 と同時に 3.1.0/2.1.0)。
@spree/sdk には、@spree/sdk/types(生成された型)、@spree/sdk/zod(Zod スキーマ)、@spree/sdk/webhooks(署名検証、Node 専用)も含まれます。Admin SDK と Seller SDK にも対応する /types があり、1.0.0-beta.3 の次のリリースからは /zod サブパス(@spree/admin-sdk/zod、@spree/seller-sdk/zod)も利用できます。サブパスを解決できない場合は、インストール済みのバージョンがそれより前のものです。スキーマ名は <Type>Schema で、API シリアライザーから生成されます。zod(>= 3)はオプションのピア依存関係なので、実行時にレスポンスを検証するには、ご自身でインストールしてください。
import { OrderSchema } from '@spree/admin-sdk/zod'
const order = OrderSchema.parse(await adminClient.orders.get('or_xxx')) // throws on a shape mismatch
@spree/sdk — Store API
import { createClient } from '@spree/sdk'
const client = createClient({
baseUrl: 'https://shop.example.com',
publishableKey: 'pk_xxx',
// optional defaults, sent as X-Spree-* headers
country: 'DE', locale: 'de', currency: 'EUR', channel: 'online',
})
const { data: products, meta } = await client.products.list({ name_cont: 'shirt', limit: 24, expand: ['default_variant'] })
const product = await client.products.get('classic-tee') // id or slug
セッターを使うと、以降のリクエストのデフォルト値を変更できます:client.setLocale('fr')、setCurrency、setCountry、setChannel。各クライアントインスタンスは独自のデフォルト値を保持するため、サーバー側レンダリング用クライアントとブラウザ用クライアントには、それぞれ個別に設定する必要があります。
メソッドの語彙
list(params?, options?)、get(idOrSlug, params?, options?)、create(body, options?)、update(id, body, options?)、delete(id, options?)。メソッド名は常に get(show ではありません)と delete(destroy ではありません)です。ネストされたリソースでは、最初に親 ID を指定します:client.carts.items.create(cartId, body, options)。
Store SDK が公開するのは、Store API で許可されているものだけです。メソッドが型チェックを通らない場合、その API サーフェスには存在しません。
| リソース | 利用できる機能 |
|---|---|
products、categories、collections(+ collections.products.list)、sellers | カタログの読み取り(ファセットには products.filters) |
markets、countries、currencies、locales、channel、policies、deliveryMethods | コンテキストと参照データ |
carts(+ items、discountCodes、giftCards、fulfillments、payments、paymentSessions、storeCredits)、carts.associate、carts.complete | カートとチェックアウト |
orders.get(+ returns、claims) | 完了した注文 |
auth、customers.create、passwordResets、customer.*(profile、addresses、creditCards、giftCards、storeCredits、digitalLinks、orders、paymentSetupSessions) | アカウント |
wishlists(+ items) | ウィッシュリスト |
newsletterSubscribers(create、verify、requestUnsubscribe、delete) | ダブルオプトインのニュースレター |
account.companies、companies(+ addresses、members、invitations、orders)、companyInvitations(lookup、accept) | B2B セルフサービス(spree-b2b を参照)。companies.members.create(id, { customer_email }) は常に CompanyInvitation を返します |
まだラッパーがないエンドポイント(例:/customers/me/data_requests、カートの tax_identifier、po_document)には、client.request(以下を参照)からアクセスできます。
認証、ゲストトークン、リクエストごとのオプション
// Customer login: tokens aren't stored on the client; pass them per request
const { token, refresh_token, user } = await client.auth.login({ email, password })
const { data: orders } = await client.customer.orders.list({}, { token })
const fresh = await client.auth.refresh({ refresh_token }) // rotates both tokens
await client.auth.logout({ refresh_token })
// Third-party IdP (strategy registered on the server)
await client.auth.login({ provider: 'external_idp', token: idpJwt })
RequestOptions:token(Bearer JWT)、spreeToken(ゲストカート/注文トークン。X-Spree-Token として送信)、idempotencyKey、headers。
カート → 注文
let cart = await client.carts.create()
const opts = { spreeToken: cart.token } // or { token } for a signed-in customer
cart = await client.carts.items.create(cart.id, { variant_id: 'variant_xxx', quantity: 1 }, opts)
cart = await client.carts.update(cart.id, { email, shipping_address, use_shipping: true }, opts)
// cart.requirements → [{ step, field, code, message }] (what still blocks completion)
const result = await client.carts.complete(cart.id, opts) // Order (or_…, order.cart_id === cart.id) | OrderGroup (ogrp_…)
- 完了後、カートエンドポイントからカートが消えます。
client.carts.get(cartId)は 404 のSpreeError(cart_not_found)をスローします。確認ページでは、またはリダイレクト方式の決済後に、client.orders.get(cartId, {}, opts)を読み取ってください。注文エンドポイントはカート ID を受け取り、同じゲストトークンを使って該当する注文を解決します。 - チェックアウト UI はステートマシンではなく、
cart.requirementsに基づいて制御してください(spree-checkoutを参照)。 Orderはnumber、cart_id、coupon_code、payment_status、fulfillment_status、合計金額、display_*を公開します。Store シリアライザーにはstateとstatusはありません。carts.completeの型はPromise<Order | OrderGroup>です。複数の販売者がいるマーケットプレイスのカートは、注文グループ(ogrp_…、orders[]を含む)に変換されます。@spree/sdkからエクスポートされるisOrderGroupガードを使って型を絞り込んでください。const orders = isOrderGroup(result) ? result.orders : [result]。マーケットプレイスではないストアでは常にOrderが返されますが、TypeScript では引き続き型を絞り込む必要があります。(@spree/admin-sdkには、管理者向けの完了処理用に独自のisOrderGroupがあります。)
金額は string | null です
金額は、display_* の関連フィールドとともに小数の文字列("29.99")で表されます。ゲストに価格を表示しないチャネルでは null となり、生成される型も string | null です。null を処理し、無条件に parseFloat を使わないでください。
リストのパラメーター(Ransack)
await client.products.list({ page: 2, limit: 50, name_cont: 'shirt', sort: '-created_at', expand: ['media'], fields: ['name', 'slug'] })
page/limit/sort/expand/fields 以外のキーは q[...] の述語になります(name_cont → q[name_cont]。配列の場合は [] が付きます)。述語を filter キーの下にネストしないでください。
エラー
import { SpreeError } from '@spree/sdk'
try { await client.carts.items.create(cartId, { variant_id, quantity: 1 }, opts) }
catch (err) {
if (err instanceof SpreeError) {
err.status // 422
err.code // 'insufficient_stock' | 'validation_error' | ...
err.details // { quantity: ['...'] } (attribute keys; 'base' = form-level)
}
throw err
}
再試行と冪等性
デフォルトでは、429/500/502/503/504 とネットワークエラーが発生した場合に、指数バックオフとジッターを使って 2 回再試行し、Retry-After に従います。再試行が有効な場合、変更を伴うリクエストには Idempotency-Key が自動的に付与されるため、POST を再試行しても処理が二重に適用されることはありません。retry: { maxRetries, baseDelay, maxDelay, retryOnStatus, retryOnNetworkError } で設定するか、retry: false で無効にできます。Next.js のキャッシュ、トレース、テストには fetch を渡してください。
@spree/admin-sdk — 管理 API
import { createAdminClient } from '@spree/admin-sdk'
// Integration (server only)
const admin = createAdminClient({ baseUrl, secretKey: process.env.SPREE_SECRET_KEY! })
const { data: orders } = await admin.orders.list({ status_eq: 'placed', sort: '-completed_at' })
const order = await admin.orders.get('or_xxx', { expand: ['items', 'payments', 'fulfillments'] })
await admin.orders.cancel('or_xxx', { cancel_reason_id: 'ocr_xxx', cancel_note: 'Customer request' })
await admin.orders.refunds.create('or_xxx', { payment_id: 'py_xxx', amount: '25.00', refund_reason_id: 'rr_xxx' }) // refund_reason_id, not reason_id
- シークレットキー:スコープが制限されています。スコープが不足していると、
details.required_scopeを伴う 403 がスローされます。可能な限り範囲の狭いキーを発行してください(spree-api-v3/references/scopes.mdを参照)。 - スタッフ JWT(カスタムのバックオフィス UI 用):
admin.auth.login({ email, password })(または{ provider, ... })は{ token, user }を返し、リフレッシュトークンは HttpOnly Cookie に設定されます。その後、admin.setToken(token)を呼び出します。admin.auth.refresh()にはトークン引数がありません(Cookie が使用されます)。管理者 JWT の有効期限は 5 分のため、admin.onUnauthorized(async () => { const { token } = await admin.auth.refresh(); admin.setToken(token); return true })を設定してください。クライアントのデフォルト値はcredentials: 'include'です。スタッフのアクセスはロールの権限キーで制御されます(details.required_permissionを伴う 403)。spree-auth-permissionsを参照してください。 - マルチストアのホストでは、
admin.setStore(storeId)によってX-Spree-Store-Idが送信されます。 - 完全な CRUD に加え、ドメイン固有のアクションも利用できます:
orders.complete/cancel/approve、products.clone/bulkStatusUpdate/bulkAddToCategories/...、prices.bulkUpsert、stockLevels.bulkUpsert、roles、permissions.list()、exports、imports、webhookEndpoints、apiKeysなど(packages/admin-sdk/src/admin-client.tsを参照)。 - 招待一覧には
acceptance_urlが含まれません。必要に応じてadmin.invitations.acceptanceLink(id)(またはadmin.sellers.invitations.acceptanceLink(sellerId, id))でリンクを取得してください。これは書き込み権限が必要です。admin.invitations.createにはrole_idが必要です。 - キーまたはスタッフメンバーに読み取り権限がない場合、注文/支払い/顧客/ギフトカード/ストアクレジットの
?expand=は通知なく無視されます。API がデータを返さなかったと判断する前に、スコープを確認してください。 - 在庫には
stockItemsではなくadmin.stockLevels(sl_…)を使用します。住所ではcountry_isoではなくcountry_code/state_codeを使用します。 - 商品の価格は、シンプルな商品では
prices: [{ currency: 'USD', amount: '29.99' }]の形式で指定します。バリアントごとの指定も可能です。 - シングルトンには
getを使用します:admin.me.get()(permission_keysを含む)、admin.store.get()。 - エクスポート:
admin.exports.create({ type, search_params })を実行し、完了するまでexports.get(id)をポーリングします。その後、Authorizationヘッダーを付けて fetch でダウンロードし、Blob に格納します。.download()ヘルパーはありません。
Webhook レシーバー
// app/api/webhooks/spree/route.ts (Node runtime)
import { verifyWebhookSignature, type WebhookEvent } from '@spree/sdk/webhooks'
import type { Order } from '@spree/sdk'
export async function POST(request: Request) {
const rawBody = await request.text() // raw bytes, before JSON.parse
const ok = verifyWebhookSignature(
rawBody,
request.headers.get('x-spree-webhook-signature') ?? '',
request.headers.get('x-spree-webhook-timestamp') ?? '',
process.env.SPREE_WEBHOOK_SECRET!,
300, // replay tolerance (seconds)
)
if (!ok) return new Response('Unauthorized', { status: 401 })
const event = JSON.parse(rawBody) as WebhookEvent<Order>
if (event.name === 'order.placed') await syncToERP(event.data)
return Response.json({ ok: true })
}
order.placed を購読してください。order.completed は非推奨のエイリアスとして 6.0 を通じて引き続き送信されます(そのメタデータには deprecated_alias_of が含まれます)が、6.1 で削除されます。検証処理では node:crypto を使用します。Edge ランタイムでは、Web Crypto を使って `${timestamp}.${rawBody}` に対する HMAC-SHA256 を実装してください(spree-events-webhooks を参照)。失敗した配信はバックオフを伴って再試行され(最大 5 回、同じ event.id)、そのためハンドラーは event.id に基づいて冪等にしてください。
カスタムエンドポイント向けの SDK 拡張
SDK をフォークしたり、生の fetch でバイパスしたりしないでください。認証ヘッダー、ロケール/通貨/チャネルのデフォルト値、リトライ、冪等性、エラーの正規化が適用されなくなります。
1. 単発の呼び出し:client.request
パスはサーフェスのルート(/api/v3/store または /api/v3/admin)からの相対パスです。request は transformListParams を適用しないため、Ransack の述語は自分でラップしてください。
import type { PaginatedResponse } from '@spree/sdk'
type Brand = { id: string; name: string; slug: string }
const { data } = await client.request<PaginatedResponse<Brand>>('GET', '/brands', {
params: { 'q[name_cont]': 'acme', page: 1, limit: 25 },
})
const created = await admin.request<Brand>('POST', '/brands', { body: { name: 'Acme' } })
2. ラップしたリソースクラス
import type { RequestFn, PaginatedResponse } from '@spree/sdk'
export class BrandsClient {
constructor(private readonly request: RequestFn) {}
list(params: { page?: number; limit?: number; name_cont?: string } = {}) {
const { page, limit, ...predicates } = params
const q = Object.fromEntries(Object.entries(predicates).map(([k, v]) => [`q[${k}]`, v]))
return this.request<PaginatedResponse<Brand>>('GET', '/brands', { params: { page, limit, ...q } })
}
get(id: string) { return this.request<Brand>('GET', `/brands/${id}`) }
}
@spree/admin-sdk は RequestFn をエクスポートしないため、管理者向けのラッパーは AdminClient['request'] を使って型付けしてください。
3. クライアントへの追加
import { createClient, type Client, type ClientConfig } from '@spree/sdk'
export interface ExtendedClient extends Client { brands: BrandsClient }
export function createExtendedClient(config: ClientConfig): ExtendedClient {
const client = createClient(config) as ExtendedClient
client.brands = new BrandsClient(client.request)
return client
}
組み込みリソースにメソッドを追加するには、Object.assign(base, { products: { ...base.products, republish(id: string) { ... } } }) を使用してください。リソースはプロトタイプ上にあり、破棄されてしまうため、クライアント自体を展開する { ...base } は使用しないでください。
カスタムリソースの型
spree:api_resource によって生成されるシリアライザーは、Typelizer を通じて型付けされます(API エンジンで bundle exec rake typelizer:generate を実行)。スタンドアロンアプリでは、Typelizer の出力先を独自のフォルダーに設定するか、シリアライザーに一致するインターフェースを手動で記述してください。シリアライザーが信頼できる唯一の情報源です。
MSW を使ったテスト
SDK は fetch を呼び出すだけなので、すべてのリクエストは MSW のハンドラーで確認できます:
server.use(
http.get('https://test.spree.local/api/v3/store/products', () =>
HttpResponse.json({ data: [{ id: 'prod_test1', name: 'Test' }], meta: { page: 1, limit: 25, count: 1, pages: 1 } })),
)
const client = createClient({ baseUrl: 'https://test.spree.local', publishableKey: 'pk_test' })
よくある問題
- チェックアウト後に
carts.getで 404 が発生する。 想定どおりです。orders.get(cartId)を使用してください。 - 合計金額が「NaN」と表示される。 チャネルで価格が非表示になっており、フィールドが
nullになっています。 - 管理者リクエストが 5 分ごとに 401 になる。
onUnauthorizedを登録していないか、更新用クッキーがブロックされています。クロスオリジン構成では、HTTPS と Settings → Allowed Origins に登録したオリジンが必要です。 - シークレットキーを使うと 403 になる。
err.details.required_scopeを確認し、そのスコープを持つキーを発行してください。既存のキーのスコープは編集できません。 - ブラウザーバンドルにシークレットキーを含めている。 絶対にやめてください。
secretKeyを使う@spree/admin-sdkはサーバー上でのみ使用し、ダッシュボードでは JWT フローを使用します。 order.completedをリッスンしている。order.placedに切り替えてください。
詳細情報
- Store SDK のソース:
packages/sdk/src/store-client.ts。Admin SDK:packages/admin-sdk/src/admin-client.ts。Seller SDK:packages/seller-sdk/src/seller-client.ts - ドキュメント:
node_modules/@spree/docs/dist/developer/sdk/(Store クイックスタート、admin/quickstart.md、admin/authentication.md) - OpenAPI:
node_modules/@spree/docs/dist/api-reference/store.yaml、admin.yaml、seller.yaml - 関連スキル:
spree-api-v3、spree-storefront、spree-checkout、spree-events-webhooks、spree-auth-permissions、spree-marketplace。