はじめに
ヘッドレスE-commerceにおいて、カートとチェックアウトは購買プロセスの中心であり、ユーザー体験(UX)とコンバージョン率に直接影響します。この第4部では、Shopify Storefront APIのGraphQLを使用して、Next.jsでカート管理とチェックアウトフローを実装する方法を解説します。スケーラブルで高性能な購買体験を構築するため、状態管理(Zustand)、ミューテーション、およびサーバーレスAPIを活用します。開発者向けに、パフォーマンス最適化とエラーハンドリングを重視したコードスニペットを提供します。
カートとチェックアウトの重要性
E-commerceの購買フローでは、以下の要素が重要です:
- スムーズなカート操作: 商品の追加、更新、削除が直感的。
- 高速なチェックアウト: 最小限のステップで支払い完了。
- 状態の一貫性: クライアントとサーバー間のデータ同期。
- エラーハンドリング: 在庫不足や支払いエラーの適切な処理。
Shopify Storefront APIは、カートとチェックアウトを管理するための強力なGraphQLミューテーションを提供し、Next.jsのAPI Routesと組み合わせることで柔軟な実装が可能です。
カート管理の実装
カートは、ユーザーが商品を選択し、購入に進む前の中間ステップです。Zustandを使用してクライアント側の状態を管理し、Shopify Buy SDKとGraphQLでバックエンドと同期します。
1. Zustandでのカート状態管理
Zustandは軽量でシンプルな状態管理ライブラリです。カートの状態を管理:
# Zustandのインストール
npm install zustand
カートストアの設定(lib/cart-store.ts
):
import { create } from 'zustand';
interface CartItem {
id: string;
title: string;
quantity: number;
price: { amount: string; currencyCode: string };
}
interface CartState {
items: CartItem[];
checkoutId: string | null;
addItem: (item: CartItem) => void;
updateQuantity: (id: string, quantity: number) => void;
removeItem: (id: string) => void;
setCheckoutId: (id: string) => void;
}
export const useCartStore = create<CartState>((set) => ({
items: [],
checkoutId: null,
addItem: (item) => set((state) => ({
items: [...state.items.filter((i) => i.id !== item.id), item],
})),
updateQuantity: (id, quantity) => set((state) => ({
items: state.items.map((item) =>
item.id === id ? { ...item, quantity } : item
),
})),
removeItem: (id) => set((state) => ({
items: state.items.filter((item) => item.id !== id),
})),
setCheckoutId: (id) => set({ checkoutId: id }),
}));
2. Shopify Buy SDKでのカート操作
Shopify Buy SDKを使用して、カートを作成し商品を追加:
# Shopify Buy SDKのインストール
npm install @shopify/buy-button-js
カート初期化(lib/shopify-cart.ts
):
import Client from 'shopify-buy';
const client = Client.buildClient({
domain: 'my-ecommerce-dev.myshopify.com',
storefrontAccessToken: process.env.SHOPIFY_STOREFRONT_API_TOKEN,
});
export async function createCart() {
const checkout = await client.checkout.create();
return checkout;
}
export async function addToCart(checkoutId: string, variantId: string, quantity: number) {
const lineItems = [{ variantId, quantity }];
const updatedCheckout = await client.checkout.addLineItems(checkoutId, lineItems);
return updatedCheckout;
}
export async function updateCartItem(checkoutId: string, lineItemId: string, quantity: number) {
const updatedCheckout = await client.checkout.updateLineItems(checkoutId, [
{ id: lineItemId, quantity },
]);
return updatedCheckout;
}
export async function removeCartItem(checkoutId: string, lineItemId: string) {
const updatedCheckout = await client.checkout.removeLineItems(checkoutId, [lineItemId]);
return updatedCheckout;
}
3. カートUIの実装
カートコンポーネントを作成し、Zustandで状態を管理:
// components/Cart.tsx
import { useCartStore } from '../lib/cart-store';
import { addToCart, updateCartItem, removeCartItem } from '../lib/shopify-cart';
export default function Cart() {
const { items, checkoutId, addItem, updateQuantity, removeItem, setCheckoutId } = useCartStore();
const handleAddToCart = async (variantId: string, title: string, price: { amount: string; currencyCode: string }) => {
if (!checkoutId) {
const checkout = await createCart();
setCheckoutId(checkout.id);
const updatedCheckout = await addToCart(checkout.id, variantId, 1);
addItem({ id: variantId, title, quantity: 1, price });
return;
}
const updatedCheckout = await addToCart(checkoutId, variantId, 1);
addItem({ id: variantId, title, quantity: 1, price });
};
const handleUpdateQuantity = async (id: string, quantity: number) => {
const updatedCheckout = await updateCartItem(checkoutId!, id, quantity);
updateQuantity(id, quantity);
};
const handleRemoveItem = async (id: string) => {
const updatedCheckout = await removeCartItem(checkoutId!, id);
removeItem(id);
};
return (
<div className="container mx-auto p-4">
<h1 className="text-3xl font-bold mb-6">カート</h1>
{items.length === 0 ? (
<p>カートは空です</p>
) : (
<div className="space-y-4">
{items.map((item) => (
<div key={item.id} className="flex justify-between border-b py-2">
<div>
<h2 className="text-lg font-semibold">{item.title}</h2>
<p>{item.price.amount} {item.price.currencyCode}</p>
</div>
<div className="flex items-center space-x-4">
<input
type="number"
value={item.quantity}
onChange={(e) => handleUpdateQuantity(item.id, parseInt(e.target.value))}
className="w-16 border p-1"
min="1"
/>
<button
onClick={() => handleRemoveItem(item.id)}
className="text-red-600 hover:text-red-800"
>
削除
</button>
</div>
</div>
))}
<a href={checkoutId ? `/checkout/${checkoutId}` : '#'} className="btn-primary mt-4">
チェックアウト
</a>
</div>
)}
</div>
);
}
このコンポーネントは、カートの内容を動的に表示し、数量の更新や削除をサポートします。
チェックアウトフローの実装
Shopify Storefront APIのGraphQLミューテーションを使用して、チェックアウトプロセスを実装します。
1. チェックアウトの作成
チェックアウトを作成し、配送先情報を追加するミューテーション:
mutation CreateCheckout($input: CheckoutCreateInput!) {
checkoutCreate(input: $input) {
checkout {
id
webUrl
}
checkoutUserErrors {
code
field
message
}
}
}
# 変数
{
"input": {
"lineItems": [
{ "variantId": "gid://shopify/ProductVariant/123456789", "quantity": 1 }
],
"shippingAddress": {
"firstName": "太郎",
"lastName": "山田",
"address1": "東京都渋谷区1-2-3",
"city": "渋谷区",
"country": "JP",
"zip": "150-0001"
}
}
}
Next.js API Routeでチェックアウトを作成(pages/api/checkout.ts
):
import type { NextApiRequest, NextApiResponse } from 'next';
import { gql } from '@apollo/client';
import { client } from '../../lib/apollo-client';
const CREATE_CHECKOUT = gql`
mutation CreateCheckout($input: CheckoutCreateInput!) {
checkoutCreate(input: $input) {
checkout { id webUrl }
checkoutUserErrors { code field message }
}
}
`;
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed' });
}
const { items, shippingAddress } = req.body;
try {
const { data } = await client.mutate({
mutation: CREATE_CHECKOUT,
variables: {
input: {
lineItems: items.map((item: any) => ({
variantId: item.id,
quantity: item.quantity,
})),
shippingAddress,
},
},
});
if (data.checkoutCreate.checkoutUserErrors.length > 0) {
return res.status(400).json({ errors: data.checkoutCreate.checkoutUserErrors });
}
return res.status(200).json({ checkout: data.checkoutCreate.checkout });
} catch (error) {
return res.status(500).json({ error: 'Failed to create checkout' });
}
}
2. チェックアウトページ
チェックアウトページで配送先を入力し、ShopifyのチェックアウトURLにリダイレクト:
// pages/checkout/[id].tsx
import { useRouter } from 'next/router';
import { useState } from 'react';
export default function CheckoutPage() {
const router = useRouter();
const { id } = router.query;
const [shippingAddress, setShippingAddress] = useState({
firstName: '',
lastName: '',
address1: '',
city: '',
country: 'JP',
zip: '',
});
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
const response = await fetch('/api/checkout', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ items: useCartStore.getState().items, shippingAddress }),
});
const { checkout, errors } = await response.json();
if (errors) {
console.error(errors);
return;
}
window.location.href = checkout.webUrl; // Shopifyチェックアウトにリダイレクト
};
return (
<div className="container mx-auto p-4">
<h1 className="text-3xl font-bold mb-6">チェックアウト</h1>
<form onSubmit={handleSubmit} className="space-y-4 max-w-lg">
<input
type="text"
placeholder="名"
value={shippingAddress.firstName}
onChange={(e) => setShippingAddress({ ...shippingAddress, firstName: e.target.value })}
className="border p-2 w-full"
/>
<input
type="text"
placeholder="姓"
value={shippingAddress.lastName}
onChange={(e) => setShippingAddress({ ...shippingAddress, lastName: e.target.value })}
className="border p-2 w-full"
/>
<input
type="text"
placeholder="住所"
value={shippingAddress.address1}
onChange={(e) => setShippingAddress({ ...shippingAddress, address1: e.target.value })}
className="border p-2 w-full"
/>
<input
type="text"
placeholder="市区町村"
value={shippingAddress.city}
onChange={(e) => setShippingAddress({ ...shippingAddress, city: e.target.value })}
className="border p-2 w-full"
/>
<input
type="text"
placeholder="郵便番号"
value={shippingAddress.zip}
onChange={(e) => setShippingAddress({ ...shippingAddress, zip: e.target.value })}
className="border p-2 w-full"
/>
<button type="submit" className="btn-primary">支払いに進む</button>
</form>
</div>
);
}
エラーハンドリングとテスト
以下のテストを実施:
- カートテスト: 商品の追加、更新、削除が正しく同期するか。
- チェックアウトテスト: 配送先入力後のリダイレクトが正常か。
- エラーハンドリング: 在庫不足や無効なバリエーションIDの処理。
- パフォーマンス: APIレスポンス時間(目標:200ms以下)。
エラーハンドリング例(lib/shopify-cart.ts
に追加):
export async function addToCart(checkoutId: string, variantId: string, quantity: number) {
try {
const lineItems = [{ variantId, quantity }];
const updatedCheckout = await client.checkout.addLineItems(checkoutId, lineItems);
return updatedCheckout;
} catch (error) {
console.error('[Shopify Cart Error]:', error);
throw new Error('カートへの追加に失敗しました');
}
}
まとめ
この第4部では、Shopify Storefront APIとNext.jsを使ったカートとチェックアウトの実装を解説しました。Zustandで状態管理を行い、GraphQLミューテーションとAPI Routesでスムーズな購買フローを構築しました。次の第5部では、パフォーマンスとSEOの最適化を掘り下げます。
Qiitaの皆さん、この記事が役に立ったら「いいね」や「ストック」をお願いします!コメントで技術的な質問や提案もお待ちしています!